diff --git a/README.md b/README.md index 4d98d097a..e08b80ab5 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,21 @@ lib/ │ └─ main.dart # 程序入口 ``` +### 本地调试内购(IAP)流程 + +不需要真实购买即可在本地跑通前端内购流程: + +- 在项目根目录的环境变量文件(如 `.env`)里添加: + +``` +APP_IAP_MOCK=1 +``` + +- 作用:在 `PayService` 中启用“本地内购模拟”,调用 `processGooglePayment` 或 `processApplePayment` 会: + - 立即触发“已发起内购(本地模拟)”; + - 短暂延时后触发“支付处理中…(本地模拟)”; + - 再触发“支付成功(本地模拟)”。 + +- 注意:该模式不依赖 Google Play / App Store 安装来源,也不会进行服务器收据校验,仅用于前端 UI 与状态机验证。 + +- 关闭模拟:将 `APP_IAP_MOCK` 移除或设为 `0/false`,恢复真实内购流程。 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 915595a3f..4e946db0e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -41,7 +41,7 @@ - + @@ -109,6 +109,20 @@ + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt b/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt index 8d4f460e9..b6e861db2 100644 --- a/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt +++ b/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt @@ -19,6 +19,7 @@ class FloatingWindowService : Service() { private var windowManager: WindowManager? = null private var floatingView: View? = null private var isMinimized = true + private lateinit var layoutParams: WindowManager.LayoutParams private lateinit var sourceTextView: TextView private lateinit var translatedTextView: TextView @@ -73,7 +74,7 @@ class FloatingWindowService : Service() { setupClickListeners() // 设置窗口参数 - val layoutParams = WindowManager.LayoutParams().apply { + layoutParams = WindowManager.LayoutParams().apply { width = WindowManager.LayoutParams.WRAP_CONTENT height = WindowManager.LayoutParams.WRAP_CONTENT type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -255,26 +256,12 @@ class FloatingWindowService : Service() { } private fun setupClickListeners() { - // 最小化状态点击展开 - minimizedView.setOnClickListener { - isMinimized = false - updateViewState(false) - notifyStateChange("expanded") - } - - // 展开状态点击最小化 - expandedView.setOnClickListener { - isMinimized = true - updateViewState(true) - notifyStateChange("minimized") - } - // 长按关闭 floatingView!!.setOnLongClickListener { closeFloatingWindow() true } - + // 拖拽功能 setupDragListener() } @@ -284,25 +271,40 @@ class FloatingWindowService : Service() { var initialY = 0 var initialTouchX = 0f var initialTouchY = 0f - + var dragging = false + val threshold = (4 * resources.displayMetrics.density).toInt() floatingView!!.setOnTouchListener { _, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { - val layoutParams = floatingView!!.layoutParams as WindowManager.LayoutParams initialX = layoutParams.x initialY = layoutParams.y initialTouchX = event.rawX initialTouchY = event.rawY + dragging = false true } MotionEvent.ACTION_MOVE -> { - val layoutParams = floatingView!!.layoutParams as WindowManager.LayoutParams - layoutParams.x = initialX + (event.rawX - initialTouchX).toInt() - layoutParams.y = initialY + (event.rawY - initialTouchY).toInt() + val dx = (event.rawX - initialTouchX).toInt() + val dy = (event.rawY - initialTouchY).toInt() + if (Math.abs(dx) > threshold || Math.abs(dy) > threshold) dragging = true + val dm = resources.displayMetrics + val maxX = dm.widthPixels - (floatingView?.width ?: 0) + val maxY = dm.heightPixels - (floatingView?.height ?: 0) + layoutParams.x = (initialX + dx).coerceIn(0, maxX) + layoutParams.y = (initialY + dy).coerceIn(0, maxY) windowManager?.updateViewLayout(floatingView, layoutParams) true } - else -> false + MotionEvent.ACTION_UP -> { + if (!dragging) { + isMinimized = !isMinimized + updateViewState(isMinimized) + notifyStateChange(if (isMinimized) "minimized" else "expanded") + } + dragging = false + true + } + else -> true } } } @@ -347,11 +349,10 @@ class FloatingWindowService : Service() { val x = intent.getIntExtra(EXTRA_X, 0) val y = intent.getIntExtra(EXTRA_Y, 0) - floatingView?.let { view -> - val layoutParams = view.layoutParams as WindowManager.LayoutParams + floatingView?.let { layoutParams.x = x layoutParams.y = y - windowManager?.updateViewLayout(view, layoutParams) + windowManager?.updateViewLayout(it, layoutParams) } } diff --git a/assets/images/DefaultPicture.jpg b/assets/images/DefaultPicture.jpg new file mode 100644 index 000000000..ef039c301 Binary files /dev/null and b/assets/images/DefaultPicture.jpg differ diff --git a/assets/images/defaultPicture.png b/assets/images/defaultPicture.png new file mode 100644 index 000000000..57953cc0b Binary files /dev/null and b/assets/images/defaultPicture.png differ diff --git a/assets/images/headphone_new3.png b/assets/images/headphone_new3.png deleted file mode 100644 index 6906348ce..000000000 Binary files a/assets/images/headphone_new3.png and /dev/null differ diff --git a/assets/images/headphone_new4.png b/assets/images/headphone_new4.png deleted file mode 100644 index 7161868f8..000000000 Binary files a/assets/images/headphone_new4.png and /dev/null differ diff --git a/doc/yhxy.html b/doc/yhxy.html new file mode 100644 index 000000000..1c2827bca --- /dev/null +++ b/doc/yhxy.html @@ -0,0 +1,170 @@ + + + + + + Voitrans应用用户协议 + + + +

Voitrans应用用户协议

+ +

这些使用条款和条件告诉您如何使用Voitrans应用程序和在线服务("服务")进行访问。深圳市良闯时代科技有限公司(以下简称"良闯")在此特别提醒用户认真阅读、充分理解本《用户协议》(以下简称"本协议")。

+ +

用户应认真阅读、充分理解本协议中各条款。请您审慎阅读并选择接受或不接受本协议。您使用Voitrans APP(以下简称"本APP"或"本软件")等行为将视为对本协议的接受,并同意接受本协议各项条款的约束。如您不同意本协议,请勿使用本APP。良闯有权修订本协议,更新后的协议条款将公布于本APP,自公布之日起生效。

+ +

一、总则

+

1.1 本APP是良闯提供的用于连接良闯及其合作公司发布的耳机产品的软件,本软件及相关服务的所有权和运营权均归良闯所有。

+ +

1.2 本协议是您(以下简称"用户")与良闯及其合作公司以及运营合作单位(以下简称"合作单位")之间关于用户使用本APP及相关服务所订立的协议。

+ +

1.3 本软件及服务包括:

+

1.3.1 提供的软件为使用Voitrans的用户提供Voitrans的连接与绑定、功能设置管理等软件服务,具体以本APP 实际提供的软件服务为准。

+

1.3.2 本软件的更新、升级以及其他服务。

+ +

二、软件授权范围

+

2.1 良闯就本软件给予用户一项个人的、不可转让、不可转授权以及非独占性的许可。

+ +

2.2 用户可以为非商业目的在单一台移动终端设备上安装、使用、显示、运行本软件。但用户不得为商业运营目的安装、使用、运行本软件,不可以对本软件或者本软件运行过程中释放到任何终端设备内存中的数据及本软件运行过程中客户端与服务器端的交互数据进行复制、更改、修改、挂接运行或创作任何衍生作品,形式包括但不限于使用插件、外挂或非经授权的第三方工具/服务接入本软件和相关系统。如果需要进行商业性的销售、复制和散发,例如软件预装和捆绑,必须获得良闯的书面授权和许可。

+ +

2.3 用户不得未经良闯许可,将本软件安装在未经良闯明示许可的其他终端设备上,包括但不限于电视、数字机顶盒、游戏机等装置。

+ +

2.4 除本协议明示授权外,良闯未授权给用户其他权利,若用户使用其他权利时须另外取得良闯的书面同意。

+ +

三、软件升级

+

3.1 用户必须选择适合所安装之终端设置的软件版本。否则,任何软件版本与装置机型不适合所引起的软件问题、装置问题或损坏,应完全由用户自行负责。

+ +

3.2 为了改善用户体验、完善服务内容,良闯有权不时地为您提供本软件替换、修改、升级版本,本软件为用户默认开通"升级提示"功能,视用户使用的软件版本差异,良闯提供给用户自行选择是否需要开通此功能。软件新版本发布后,良闯不保证旧版本软件的继续可用。

+ +

四、使用规范

+

4.1 用户在遵守法律及本协议的前提下可依本协议使用本软件及服务,用户不得实施如下行为:

+ +

4.1.1 删除本软件及其他副本上一切关于版权的信息,以及修改、删除或避开本软件为保护知识产权而设置的技术措施。

+ +

4.1.2 对本软件进行反向工程,如反汇编、反编译或者其他试图获得本软件的源代码。

+ +

4.1.3 通过修改或伪造软件运行中的指令、数据,增加、删减、变动软件的功能或运行效果,或者将用于上述用途的软件、方法进行运营或向公众传播,无论这些行为是否为商业目的。

+ +

4.1.4 使用本软件进行任何危害网络安全的行为,包括但不限于:使用未经许可的数据或进入未经许可的服务器/账户;未经允许进入公众网络或者他人操作系统并删除、修改、增加存储信息;未经许可企图探查、扫描、测试本软件的系统或网络的弱点或其它实施破坏网络安全的行为;企图干涉、破坏本软件系统或网站的正常运行,故意传播恶意程序或病毒以及其他破坏干扰正常网络信息服务的行为;伪造 TCP/IP 数据包名称或部分名称。

+ +

4.1.6 未经良闯书面同意,用户对软件及其中的信息擅自实施包括但不限于下列行为:使用、出租、出借、复制、修改、链接、转载、汇编、发表、出版,建立镜像站点、擅自借助本软件发展与之有关的衍生产品、作品、服务、插件、外挂、兼容、互联等。

+ +

4.1.7 利用本软件发表、传送、传播、储存违反您当地法律的内容。

+ +

4.1.8 利用本软件发表、传送、传播、储存侵害他人知识产权、商业秘密等合法权利的内容。

+ +

4.1.9 利用本软件批量发表、传送、传播广告信息及垃圾信息。

+ +

4.1.10 其他以任何不合法的方式、为任何不合法的目的、或以任何与本协议许可使用不一致的方式使用本软件和良闯提供的其他服务。

+ +

4.2 信息发布规范

+ +

4.2.1 您使用本软件发表信息内容的(如有),您应保证,您拥有您所上传信息内容的知识产权或已获得合法授权,您使用本软件及服务的任何行为未侵犯任何第三方之合法权益。

+ +

4.2.2 用户在使用本软件时不得利用本软件从事以下行为,包括但不限于:

+ +

4.2.2.1 制作、复制、发布、传播、储存违反当地国家法律法规的内容。

+ +

4.2.2.2 发布、传送、传播、储存侵害他人名誉权、肖像权、知识产权、商业秘密等合法权利的内容。

+ +

4.2.2.3 虚构事实、隐瞒真相以误导、欺骗他人。

+ +

4.2.2.4 发表、传送、传播广告信息及垃圾信息。

+ +

4.2.2.5 从事其他违反当地法律法规的其他行为。

+ +

4.2.3 未经良闯许可,您不得在本软件中进行任何诸如发布广告、销售商品的商业行为。

+ +

4.3 用户理解并同意:

+ +

4.3.1 良闯会对用户是否涉嫌违反上述使用规范做出认定,并根据认定结果中止、终止对您的使用许可或采取其他依本约定可采取的限制措施。

+ +

4.3.2 对于用户使用许可软件时发布的涉嫌违法或涉嫌侵犯他人合法权利或违反本协议的信息,良闯会直接删除。

+ +

4.3.3 对于用户违反上述使用规范的行为对第三方造成损害的,您需要以自己的名义独立承担法律责任,并应确保良闯免于因此产生损失或增加费用。

+ +

4.3.4 若用户违反有关法律规定或协议约定,使良闯遭受损失,或受到第三方的索赔,或受到行政管理机关的处罚,用户应当赔偿良闯因此造成的损失和(或)发生的费用,包括合理的律师费、调查取证费用。

+ +

五、第三方服务说明

+

5.1 用户理解并同意,本软件系统可能包含由良闯的关联方或第三方提供的服务,良闯只是为了用户便利操作而在系统中提供相关功能模块,提供第三方服务的使用入口。

+ +

5.2 不论第三方服务预置于本软件系统服务中,还是由用户自行开通或订购,用户均理解并同意,良闯不对第三方服务提供方或用户行为的合法性、有效性,以及第三方服务的合法性、准确性、有效性、安全性进行任何明示或默示的保证或担保。

+ +

5.3 良闯并不监督第三方服务,不对其拥有任何控制权,也不对第三方服务提供任何形式的保证或担保,更不承担任何责任。

+ +

5.4 用户与第三方服务提供方之间发生的任何争议、纠纷应由用户与该方按照第三方服务提供方的业务流程及业务规范自行协商解决,良闯不承担任何责任。

+ +

六、隐私政策与个人信息保护

+

6.1 良闯承诺按照隐私政策保护用户的隐私,具体请见:《Voitrans应用隐私政策》。

+ +

七、违约责任

+

7.1 良闯有权判断用户的行为是否符合本协议条款规定,如果认为用户违反有关法律法规或者本协议、相关规则的规定,根据用户违规情形的严重程度,良闯有权对用户采取删除违规信息、限制、中止、终止用户使用本软件服务、追究用户的法律责任以及其他良闯认为适合的处理措施。如果使良闯遭受任何损失(包括但不限于收到任何第三方的索赔或任何行政管理部门的处罚),用户应当承担全部责任。

+ +

八、服务风险及免责声明

+

8.1 良闯以目前的技术提供服务支持。良闯不保证本软件服务在操作上不会中断或没有错误,不保证会纠正本软件服务的所有缺陷,亦不保证本软件服务能满足用户的所有要求。由此产生的后果,良闯不承担任何责任。

+ +

8.2 用户因第三方如通讯线路故障、技术问题、网络、电脑终端设备故障、系统不稳定性及其他各种不可抗力原因而遭受的一切损失,良闯不承担责任。

+ +

8.3 良闯不保证通过本软件获得的信息内容(包括但不限于调用的第三方服务内容)的合法性、真实性、准确性、有效性,良闯不对用户基于查询、查看做出的任何行为的结果承担任何责任。

+ +

8.4 本软件同大多数互联网软件一样,受包括但不限于用户原因、网络服务质量、社会环境等因素的差异影响,可能受到各种安全问题的侵扰,如他人利用用户的资料,造成现实生活中的骚扰;用户下载安装的其他软件或访问的其他网站中含有"特洛伊木马"等病毒,威胁到用户的终端设备信息和数据的安全,继而影响本软件的正常使用等等。用户应加强信息安全及使用者资料的保护意识,要注意加强密码保护,以免遭致损失和骚扰。

+ +

8.5 因用户使用本软件或要求良闯提供特定服务时,本软件可能会调用第三方系统或第三方软件支持用户的使用或访问,使用或访问的结果由该第三方提供,用户除遵守本协议相关规则外,还应遵守第三方的协议、相关规则。用户须理解并同意,在使用第三方服务时,第三方可能会对用户数据进行读取,良闯不保证通过第三方系统或第三方软件支持实现的结果的安全性、准确性、有效性及其他不确定的风险,用户应审慎判断,由此若引发的任何争议及损害,良闯不承担任何责任。

+ +

8.6 良闯特别提请用户注意,良闯为了保障公司业务发展和调整的自主权,良闯采取合理的方式通知用户后可随时修改或中断服务。

+ +

8.7 除法律法规有明确规定外,良闯将尽最大努力确保软件及其所涉及的技术及信息安全、有效、准确、可靠,但受限于现有技术,用户理解良闯不能对此进行担保。

+ +

8.8 由于用户因下述任一情况所引起或与此有关的人身伤害或附带的、间接的经济损害赔偿,包括但不限于利润损失、资料损失、业务中断的损害赔偿或其他商业损害赔偿或损失,需由用户自行承担:

+ +

(1)使用或未能使用许可软件。

+ +

(2)第三方未经许可的使用软件或更改用户的数据。

+ +

(3)用户使用软件进行的行为产生的费用及损失。

+ +

(4)用户对软件的误解。

+ +

(5)非因良闯的原因引起的与软件有关的其他损失。

+ +

8.9 用户同意良闯有权基于司法、监管部门、监督机构的要求或自身业务原因,暂停、中断或终止向用户提供全部或者部分本服务。对此良闯不承担任何责任。

+ +

8.10 用户理解并同意,良闯将会尽其商业上的合理努力保障用户在本软件及服务中的数据存储安全,但良闯不能提供完全保证,用户须理解,良闯不对服务中的数据的删除或储存或备份失败负责。

+ +

8.11 在法律允许的最大范围内,开发者或供应商不对因使用应用程序而造成的任何直接、间接、偶然、特殊或后果性的损害负责。用户如有进一步的需要,请去往正规医院进行深度检测。

+ +

九、知识产权声明

+

9.1 良闯是本软件的知识产权权利人。本软件的一切著作权、商标权、专利权、商业秘密等知识产权,以及与本软件相关的所有信息内容(包括但不限于文字、图片、音频、视频、图表、界面设计、版面框架、有关数据或电子文档等)均受当地法律法规和相应的国际条约保护,良闯享有上述知识产权。

+ +

9.2 未经良闯书面同意,用户不得为任何商业或非商业目的自行或许可任何第三方实施、利用、转让上述知识产权,良闯保留追究上述行为法律责任的权利。

+ +

十、协议变更

+

10.1 良闯有权在必要时修改本协议条款,协议条款一旦发生变动,将会在相关页面上公布修改后的协议条款。如果不同意所改动的内容,用户应主动取消此项服务。如果用户继续使用服务,则视为接受协议条款的变动。

+ +

10.2 良闯有权按需要修改或变更所提供的收费服务、收费标准、收费方式、服务费及服务条款。良闯在提供服务时,可能现在或日后对部分服务的用户开始收取一定的费用如用户拒绝支付该等费用,则不能在收费开始后继续使用相关的服务。

+ + \ No newline at end of file diff --git a/doc/yhxy_en.html b/doc/yhxy_en.html new file mode 100644 index 000000000..ad7ceffe5 --- /dev/null +++ b/doc/yhxy_en.html @@ -0,0 +1,151 @@ + + + + + + Voitrans Application User Protocol + + + +
+
English(US)
+ +

Voitrans Application User Protocol

+ +

These Terms and Conditions of Use tell you how to access using the Voitrans Application and online services (the "Services"). Saitong Industries Inc. (hereinafter referred to as "Saitong") hereby reminds users to carefully read and fully understand this User Agreement (hereinafter referred to as the "Agreement"). Users should carefully read and fully understand the terms of this Agreement. Please read carefully and choose to accept or not accept this Agreement. Your use of Voitrans APP (hereinafter referred to as "the APP" or" the Software ") will be deemed as acceptance of this Agreement and you agree to be bound by the terms of this Agreement. If you do not agree to this Agreement, do not use the APP.

+

Saitong reserves the right to amend this Agreement, and the updated terms of the Agreement will be published in this APP, effective from the date of publication.

+ +

1. General rules

+

1.1 This APP is the software provided by Saitong for connecting the headset products released by Saitong and its partner companies. The ownership and operation rights of the software and related services are all owned by Saitong.

+

1.2 This Agreement is an agreement between you (hereinafter referred to as the "User") and the company and its partners and operating partners (hereinafter referred to as the "Partners") regarding the user's use of the APP and related services.

+

1.3 The Software and services include:

+

1.3.1 The software provided provides Voitrans users with Voitrans connection and binding, function setting management and other software services, subject to the actual software services provided by the APP.

+

1.3.2 Updates, upgrades and other services of the software.

+ +

2. Scope of software license

+

2.1 We grant the user a personal, non-transferable, non-sublicensable, and non-exclusive license to the Software.

+

2.2 Users can install, use, display and run the software on a single mobile terminal device for non-commercial purposes. However, the user shall not install, use or run the software for commercial operation purposes, copy, change, modify, connect or run the software or the data released into the memory of any terminal device during the operation of the software or the interaction data between the client and the server during the operation of the software, or create any derivative works. This includes, but is not limited to, the use of plug-ins, plug-ins, or unauthorized third-party tools/services to access the Software and related systems. For commercial sale, reproduction and distribution, such as software pre-installation and bundling, written authorization and permission must be obtained from the idea.

+

2.3 The User shall not install the Software on other terminal devices without the express permission of the Creator, including but not limited to TVS, digital set-top boxes, game consoles and other devices.

+

2.4 Except as expressly authorized by this Agreement, no other rights are granted to the User, and the User shall obtain the written consent of the User to use other rights.

+ +

3. Software upgrade

+

3.1 The user must select the software version suitable for the setup of the installed terminal. Otherwise, software problems, device problems or damage caused by any software version not suitable for the device type shall be solely the responsibility of the user.

+

3.2 In order to improve the user experience and improve the service content, Saitong has the right to provide you with replacement, modification and upgrade versions of the software from time to time. The "Upgrade prompt" function is enabled for users by default. Depending on the different software versions used by users, Saitong provides users with the option to activate this function. After the release of a new version of the software, Saitong does not guarantee the continued availability of the old version of the software.

+ +

4. Usage specification

+

4.1 Users may use the Software and Services in accordance with this Agreement under the premise of abiding by the law and this Agreement, and users shall not perform the following acts:

+

4.1.1 Delete all copyright information on the Software and other copies, and modify, delete or circumvent the technical measures set up by the Software to protect intellectual property rights.

+

4.1.2 Reverse engineering the Software, such as disassembly, decompilation, or other attempts to obtain the source code of the Software.

+

4.1.3 By modifying or falsifying instructions and data in the operation of the software, increasing, deleting or changing the function or operation effect of the software, or software or methods to be used for the above purposes to operate or disseminate to the public, regardless of whether these acts are for commercial purposes.

+

4.1.4 Use the software for any behavior that jeopardizes network security, including but not limited to: using unauthorized data or accessing unauthorized servers/accounts; Access public networks or other operating systems without permission and delete, modify or add stored information; Unauthorized attempts to probe, scan, or test the weaknesses of the system or network of the software or other acts that undermine network security; Attempts to interfere with or destroy the normal operation of the software system or website, intentionally spread malicious programs or viruses, and other acts that disrupt normal network information services; Forge TCP/IP packet names or partial names.

+

4.1.6 Without the written consent of the Company, the user arbitrarily performs the following acts on the software and the information in it, including but not limited to: using, renting, lending, copying, modifying, linking, reprinting, compiling, publishing, publishing, establishing mirror sites, developing derivative products, works, services, plug-ins, plug-ins, compatibility, interconnection, etc., with the help of the Software.

+

4.1.7 Use the Software to publish, transmit, distribute, and store content that violates your local laws.

+

4.1.8 Use the software to publish, transmit, disseminate, and store content that infringes others' intellectual property rights, trade secrets, and other legitimate rights.

+

4.1.9 Use this software to publish, transmit and disseminate advertising information and junk information in bulk.

+

4.1.10 Use the Software and other Services in any unlawful manner, for any unlawful purpose, or in any manner inconsistent with the licensed use of this Agreement.

+

4.2 Information release specification

+

4.2.1 If you use the software to publish information content (if any), you should guarantee that you own the intellectual property rights of the information content uploaded by you or have obtained legal authorization, and any behavior of you using the software and services does not infringe on the legitimate rights and interests of any third party.

+

4.2.2 When using the Software, the user shall not use the Software to engage in the following activities, including but not limited to:

+

4.2.2.1 Create, copy, publish, disseminate, and store content that violates local national laws and regulations.

+

4.2.2.2 Publish, transmit, disseminate, and store content that infringes others' legal rights such as reputation, portrait, intellectual property, and trade secrets.

+

4.2.2.3 Fabricate facts, conceal the truth in order to mislead or deceive others.

+

4.2.2.4 Publish, transmit and disseminate advertising information and junk information.

+

4.2.2.5 Engaging in other acts in violation of local laws and regulations.

+

4.2.3 You may not carry out any commercial activities such as advertising and selling goods in the Software without the permission of the Company.

+

4.3 The User understands and agrees that:

+

4.3.1 Saitong will determine whether the user is suspected of violating the above specifications of use, and according to the results of the determination, suspend or terminate your license to use or take other restrictive measures that can be taken in accordance with this agreement.

+

4.3.2 For the user's use of the licensed software published suspected of illegal or suspected infringement of the legal rights of others or violation of this Agreement, the information will be directly deleted.

+

4.3.3 If the user's violation of the above rules of use causes damage to third parties, you need to bear legal responsibility independently in your own name, and you should ensure that the idea is free from losses or increased costs.

+

4.3.4 If the user violates the relevant legal provisions or agreements, causing the Saitong to suffer losses, or is subject to third-party claims, or is punished by the administrative authority, the user shall compensate the loss and/or expenses caused by the Saitong, including reasonable attorney fees, investigation and evidence collection costs.

+ +

5. Third-party service description

+

5.1 The user understands and agrees that the software system may contain services provided by affiliates of the Company or third parties, and that the Company only provides relevant functional modules in the system for the convenience of users and provides access to third-party services.

+

5.2 Whether the third-party services are preset in the software system services, or are opened or ordered by users themselves, users understand and agree that Saitong does not make any express or implied guarantee or guarantee of the legality and effectiveness of the third-party service providers or users, as well as the legality, accuracy, effectiveness and security of the third-party services.

+

5.3 The company does not supervise, control or provide any warranty or guarantee of any kind for, or assume any liability for, third party services.

+

5.4 Any dispute or dispute between the user and the third party service provider shall be resolved by the User and the third party in accordance with the business process and business norms of the third party service provider, and the company shall not assume any responsibility.

+ +

6. Privacy Policy and Protection of Personal Information

+

6.1 Saitong is committed to protecting the privacy of users in accordance with the privacy policy, please see: Voitrans applies Privacy Policy.

+ +

7. Liability for breach

+

7.1 Saitong has the right to judge whether the user's behavior complies with the provisions of this Agreement. If it is believed that the User has violated relevant laws and regulations or the provisions of this Agreement and relevant rules, according to the severity of the user's violation, Saitong has the right to delete the illegal information, restrict, suspend, terminate the user's use of the Software service, pursue the user's legal liability and take other measures that Saitong deems appropriate. If we suffer any loss (including but not limited to receiving any third party claims or penalties from any administrative department), the User shall be fully liable.

+ +

8. Service Risks and Disclaimers

+

8.1 We want to provide service support with current technology. Saitong does not warrant that the Software Service will operate without interruption or error, that all defects of the Software Service will be corrected, or that the Software Service will meet all requirements of users. The resulting consequences, the Saitong does not assume any responsibility.

+

8.2 All losses suffered by the user due to third parties such as communication line failure, technical problems, network, computer terminal equipment failure, system instability and other force majeure causes shall not be held liable.

+

8.3 Saitong does not guarantee the legality, authenticity, accuracy and effectiveness of the information content obtained through the software (including but not limited to the third party service content), and Saitong does not assume any responsibility for the results of any behavior made by users based on query and viewing.

+

8.4 The software, like most Internet software, is affected by differences in factors including but not limited to user reasons, network service quality, social environment and other factors, and may be subjected to various security problems, such as others using user information, resulting in harassment in real life; Other software downloaded and installed by users or other websites visited by users contain viruses such as "Trojan horses", which threaten the security of users' terminal equipment information and data, and then affect the normal use of the software. Users should strengthen the awareness of information security and user data protection, and pay attention to strengthen password protection to avoid loss and harassment.

+

8.5 When users use the Software or require Saitong to provide specific services, the Software may call third-party systems or third-party software to support users' use or access, and the results of use or access are provided by the third party. In addition to complying with the relevant rules of this Agreement, users shall also comply with the agreements and relevant rules of the third party. Users must understand and agree that, when using third-party services, third parties may read user data, and Saitong does not guarantee the safety, accuracy, effectiveness and other uncertain risks of the results achieved through third-party systems or third-party software support, users should exercise careful judgment, and Saitong does not assume any responsibility for any disputes and damages caused by this.

+

8.6 In order to guarantee the autonomy of the company's business development and adjustment, the company may modify or interrupt the service at any time after taking a reasonable way to notify the user.

+

8.7 Except as expressly provided by laws and regulations, Saitong will make its best efforts to ensure that the software and the technology and information involved are safe, effective, accurate and reliable, but limited by the existing technology, the user understands that Saitong cannot guarantee this.

+

8.8 Compensation for personal injury or incidental and indirect economic damage caused by or in connection with any of the following circumstances, including but not limited to loss of profits, loss of data, damages for business interruption or other business damages or losses, shall be borne by the User:

+

1) Use or failure to use the licensed software.

+

2) Unauthorized use of the software or alteration of user data by third parties.

+

3) Expenses and losses incurred by the user's actions with the software.

+

4) User misconceptions about the software.

+

5) Any other loss related to the software that is not attributable to Saitong causes.

+

8.9 The User agrees that the Company has the right to suspend, interrupt or terminate all or part of the Service to the user based on the requirements of the judicial, regulatory and supervisory authorities or its own business reasons. Saitong accepts no responsibility for this.

+

8.10 The User understands and agrees that the Company will use its commercially reasonable efforts to ensure the security of data storage in the Software and Services, but the company cannot provide a complete guarantee, and the User understands that the Company is not responsible for the deletion or storage or backup failure of data in the Service.

+

8.11 To the fullest extent permitted by law, the Developer or Supplier shall not be liable for any direct, indirect, incidental, special or consequential damages arising from the use of the Application. If the user has further needs, please go to the regular hospital for in-depth testing.

+ +

9. Intellectual property statement

+

9.1 Saitong is the intellectual property right holder of this software. All Copyrights, trademarks, patents, trade secrets and other intellectual property rights of the software, as well as all information content related to the software (including but not limited to text, pictures, audio, video, graphics, interface design, layout frame, relevant data or electronic documents, etc.) are protected by local laws and regulations and corresponding international treaties.

+

9.2 Without the written consent of the Company, the User shall not implement, utilize or transfer the above intellectual property rights for any commercial or non-commercial purpose, or permit any third party, and the Company reserves the right to pursue the legal responsibility for the above acts.

+ +

10. Protocol change

+

10.1 Saitong has the right to modify the terms of this Agreement when necessary. Once the terms of the Agreement are changed, the revised terms of the Agreement will be published on the relevant page. If you do not agree with the changed content, the user should take the initiative to cancel this service. If the User continues to use the Services, it is deemed to accept the changes in the terms of the Agreement.

+

10.2 Saitong reserves the right to modify or change the paid services, charging standards, charging methods, service fees and terms of service provided as necessary. When providing services, we may charge certain fees to users of some services now or in the future. If users refuse to pay such fees, they cannot continue to use the relevant services after the start of the fees.

+
+ + diff --git a/doc/ysxy.html b/doc/ysxy.html new file mode 100644 index 000000000..32abdf6ad --- /dev/null +++ b/doc/ysxy.html @@ -0,0 +1,397 @@ + + + + + + Voitrans隐私政策 + + + + +

Voitrans 隐私政策

+

更新日期:2025年5月27日

+

生效日期:2025年5月27日

+

深圳市良闯时代科技有限公司(下称“我们”),是VoitransAPP(具体范围及定义见下文)的开发者和运营者。我们深知个人信息对您的重要性,您的信任对我们非常重要,我们将按照法律法规的规定并参照行业最佳实践保护您的个人信息及隐私安全。我们制定本"隐私政策"并特别提示:希望您在使用Voitrans及相关服务前仔细阅读并理解本隐私政策,以便做出适当的选择。 +

+

本隐私政策将帮助您了解 :

+

我们会遵循隐私政策收集、使用您的信息,但不会仅因您同意本隐私政策而采用强制捆绑的方式收集个人信息。

+

当您使用或开启相关功能或使用服务时,为实现功能、服务所必需,我们会收集、使用相关信息。除非是为实现业务功能或根据法律法规要求所必需的必要信息,您均可以拒绝提供且不影响其他功能或服务。我们将在隐私政策中逐项说明所要收集的信息。 +

+

如果您未登录账号,我们会通过设备对应的标识符信息来保障信息推送的基本功能。如果您登录了账号,我们会根据账号信息实现信息推送。

+

精确地理位置、摄像头(相机)、相册(存储)等权限,均不会默认开启,只有经过您的明示授权才会在为实现特定功能或服务时使用,您也可以撤回授权。特别需要指出的是,即使经过您的授权,我们获得了这些敏感权限,也不会在相关功能或服务不需要时而收集您的信息。 +

+

下文将帮您详细了解我们如何收集、使用、存储、转移(如适用)与保护个人信息;帮您了解查阅、复制、更正、删除、撤回授权个人信息的方式。本政策与您使用我们的服务关系密切,我们建议您仔细阅读并理解本政策全部内容,作出您认为适当的选择,有关您个人信息权益的条款重要内容我们已用加粗形式提示 + , 请特别关注。

+

1.我们如何收集和使用个人信息

+

2 .数据使用过程中涉及的合作方以及转移、公开个人信息

+

3 .我们如何存储个人信息

+

4 .我们如何保护个人信息的安全

+

5 .我们如何管理您的个人信息

+

6 .未成年人条款

+

7 .隐私政策的修订和通知

+

8 .联系我们

+

1. 我们如何收集和使用个人信息

+

我们会按照如下方式收集您在访问Voitrans平台或使用Voitrans平台提供的产品及服务时主动提供的,以及通过自动化手段收集您在使用功能或接受服务过程中产生的信息,收集和使用这些信息的目的和用途详见下述功能描述部分。 +

+

我们不会通过共享、转让、搜集公开信息等其他渠道间接获取有关您的个人信息。如将来发生需通过其他渠道间接获取您的个人信息时,我们只获取实现产品或服务的业务功能所必需的最少数量的信息,且在收集和使用之前明确告知您并征求您的明示同意。同时,我们将在收集信息前确保信息来源的合法性,了解个人信息提供方已获得的个人信息处理的授权同意范围,包括使用目的、授权同意转让、共享、公开披露等。若我们开展业务需进行的个人信息处理活动超出该授权同意范围,会在获取个人信息后的合理期限内或处理个人信息前,征得您的明示同意。 +

+

我们收集和使用您的信息的目的和用途详情如下:

+

1.1 注册、登录Voitrans

+

当您注册、登录Voitrans时,您可以通过手机号,电子邮箱,以及微信授权方式,创建“Voitrans账号”,并且完善相关的网络身份识别信息(账号、密码、头像、昵称),我们会收集您的手机号,并且获取短信验证码,以便我们对您的账户进行身份核验。 +

+

收集您的上述信息是为了帮助您完成手机号注册和登陆,保证您的账户权益。此类信息属于使用手机号注册和登录的必要信息,如您不提供将无法完成注册/登录操作。

+

1.2 录音

+

1.2.1 必要信息范围

+

在您联网使用录音功能服务时,我们将收集您的语音信息与/或您提供的其他录音内容 。此类信息属于录音功能服务的必要信息 。请您知悉,在未联网的环境下或您未主动上传的情况下,前述信息仅会存储在设备端 + ,我们不会收集并上传云端服务器存储。

+

1.2.2 调用的权限范围

+

在您使用录音功能服务时,我们需要调用您的麦克风权限以获取您的语音信息,此为该功能服务所必须的系统权限,若您拒绝,您将无法使用录音功能 。

+

在您使用录音存储功能服务时,我们需要调用您的存储权限以存储您的语音信息,此为该功能服务所必须的系统权限,若您拒绝,您将无法使用录音功能 。

+

1.3 语音转写

+

1.3.1 必要信息范围

+

在您使用语音转文字功能服务时,我们将收集您的语音信息与/或您提供的其他录音内容以在云端服务器进行语音转写。同时,我们会收集转写后的文本信息 + ,判断您的需求并做出实时处理反馈给您,以方便您了解进行了哪些语音交互。此类信息属于语音转文字功能的必要信息,如您不能提供,您将无法使用语音转文字功能。

+

1.3.2 调用的权限范围

+

在您使用语音转文字功能服务时,我们需要调用您的麦克风权限以获取您的语音信息,此为该功能服务所必须的系统权限,若您拒绝,您将无法使用语音转文字功能。

+

1.4 管理录音及转写文稿

+

1.4.1 必要信息范围

+

当您在登录状态下使用录音转文字功能,并对以及转写文稿进行云端存储、编辑、管理时,我们将收集您的录音以及编辑后的转写文稿信息,如文本信息、语音信息,同时需获取您的网络状态,在您允许我们开启存储与网络权限后,我们可以获取到您的录音以及编辑后的转写文稿信息 + ,这是为了方便您随时导出、存储、分享您的录音以及编辑后的转写文稿。此类信息属于云端存储、编辑与管理录音及转写文稿必须的信息,如您拒绝提供,我们将无法为您提供云端存储、编辑、管理、分享、下载等服务,但不影响您使用本软件的其他功能。 +

+

1.4.2 调用的权限范围

+

在您修改录音文件时,我们需要调用您的存储权限以存储您的文本信息 ,此为该功能服务所必须的系统权限,若您拒绝,您修改后的内容将不会存储至本地端。

+

1.5 语音翻译

+

1.5.1 必要信息、非必要信息范围

+

若您使用语音翻译服务,我们会申请您设备系统的麦克风权限、存储权限 。在经您授权同意后,我们会收集您的语音信息 + ,以便/以用于获取说出的语音来进行翻译。此类信息属于语音转文字功能的必要信息,如您不能提供,您将无法使用语音翻译功能。

+

1.5.2 调用的权限范围

+

在您使用语音翻译和同声传译功能服务时我们需要调用您的麦克风权限以获取您的语音信息 ,此为该功能服务所必须的系统权限,若您拒绝,您将无法使用语音翻译和同声传译功能。

+

同时,我们需要调用您的存储权限以存储您的语音信息和文本信息,此为该功能服务所必须的系统权限,若您拒绝,您将无法使用语音翻译功能。

+

1.7 设备权限

+

1.7.1 位置权限

+

在使用智能翻译功能时,我们将收集您的定位信息以获得您的精确地理位置信息,用于实现智能翻译中的自动检测源语言功能,我们将根据您的定位信息自动匹配出您使用的语言,向您提供智能翻译服务。精确地理位置信息是个人敏感信息,若您拒绝开启GPS权限,我们将无法为您自动匹配翻译语言,需要您手动进行语言的选择。 +

+

1.7.2 蓝牙权限

+

在您连接智能硬件时,我们需要获取您的蓝牙权限来搜索设备,传输配网信息,实现绑定设备目的。

+

1.7.3 通讯录权限

+

在您使用拨打电话功能时,我们需要获取您的通讯录权限获取信息,实现您给对应用户拨打电话的需求。

+

1.7.4 短信权限

+

在您使用智能助手时,我们需要获取您的短信权限,实现您给对应用户发送短信的需求。

+

1.7.5 系统通知权限

+

在向您推送 PUSH 消息时,我们需要获取您的通知权限。这是为了能在您的设备通知栏显示 APP 相关的通知内容,包括但不限于 APP + 的新功能上线通知、您关注内容的更新信息、优惠活动提醒等。若您拒绝开启通知权限,您将无法及时收到这些重要信息,可能会错过 APP 为您准备的精彩内容和便利服务。

+

1.7.7 存储权限(较少情况)

+

在某些特殊的 PUSH + 消息场景下,若消息需要将一些临时数据存储在本地设备上,我们需要获取您的存储权限。比如推送包含新功能配置文件的消息,需要存储权限来保存这些文件到本地,以便实现新功能在您设备上的应用。若您拒绝此权限,相关特殊功能可能无法正常使用。 +

+

1.7.8 相机权限

+

当您使用设置头像或意见反馈上传图片功能时,我们会申请相机权限。在您明确授权后,相机将被用于拍摄照片以满足对应功能需求,且仅在相关功能运行期间收集和使用相机所产生的数据,功能结束后不再进行数据收集。若您拒绝授权相机权限,将无法使用涉及相机的相关功能,但不影响其他功能的正常使用。 +

+

1.7.8.1 相机权限的使用场景

+

在Voitrans App中,我们有两个主要功能,需要使用到您的相机权限:

+

更改个人头像:当您希望更改您的个人头像时,我们需要调用相机权限以便您能够拍摄新的照片或从相册中选择一张图片作为头像。

+

意见反馈功能:在意见反馈功能中,如果您希望通过上传图片来提供更详细的反馈信息,我们需要相机权限来允许您拍摄照片或从相册中选择图片上传。

+

1.7.8.2 用户控制

+

您可以随时在设备的设置中开启或关闭相机权限。如果您选择关闭相机权限,您将无法使用上述功能,但不影响您使用Voitrans App的其他功能。

+

1.7.8.3 数据的存储和处理

+

照片存储:当您使用相机权限拍摄照片或选择图片时,这些图片仅在您的设备上存储,除非您主动选择上传。

+

数据处理:如果图片被上传到我们的服务器,我们将按照隐私政策中的规定处理这些数据,包括但不限于存储、备份和删除机制。

+

1.7.8.4 数据的共享和转让

+

我们不会与任何第三方共享或转让通过相机权限收集的图片数据,除非是为了提供服务所必需,或者您明确同意。

+

1.7.8.5 用户撤销权限后的影响

+

如果您撤销相机权限,您将无法使用更改个人头像和意见反馈中上传图片的功能。如果您希望重新授权,可以在设备的设置中重新开启相机权限。

+

1.7.8.6 相机权限的安全性

+

我们承诺采取行业标准的安全措施来保护您通过相机权限提供的数据,包括但不限于数据加密传输和安全存储。

+

1.8 用户反馈与服务

+

1.8.1客服及问题

+

为便于与您联系、尽快帮助您解决问题或记录相关问题的处理方案及结果,我们可能会保存您与我们的通信/通话记录及相关内容(包括账号信息、反馈信息、预约信息、您为了证明相关事实提供的其他信息,或您留下的联系方式信息),如果您针对具体预约进行咨询、投诉或提供建议的,我们会使用您的账号信息和预约信息。 +

+

1.8.2 投诉

+

当您投诉他人或被他人投诉时,为了保护您及他人的合法权益,我们可能会将您的账号/身份信息、联系方式、投诉相关内容提供给消费者权益保护部门及监管机关,以便及时解决投诉纠纷,但法律法规明确禁止提供的除外。

+

如您通过我们产品或服务使用良闯时代关联方、合作方提供的服务并进行投诉时,为便于服务提供方(如良闯时代关联方、合作方)能与您有效联系并及时处理您的投诉,您同意我们获取您投诉时所提供的个人信息,并同步给对应服务提供方。

+

1.9 运营与安全运行/保障

+

1.9.1 运营与安全保障

+

我们致力于为您提供安全、可信的产品与使用环境,提供优质而可靠的服务与信息是我们的核心目标。为了维护Voitrans 平台的正常运行,保护您或其他用户或公众的合法利益免受损失,我们会收集用于维护产品或服务安全稳定运行的必要信息。 +

+

1.9.2 设备信息与日志信息

+

a. 为了保障软件与服务的安全运行、运营的质量及效率,我们会收集您的硬件型号、操作系统版本号、设备标识符(Android如 IMEl 、 Android_ID 、 OAID 、 IMSI 、 ICCID 、 GAID 、 + MEID 、硬件序列号, iOS 如 IDFV 、 IDFA: 不同的标识符在有效期,是否可由用户重置以及获取方式方面会有所不同) 、网络设备硬件地址 (MAC 地址 ) 、 IP 地址、 WLAN 接入点 ( 如 SSID + , BSSID) 、蓝牙 (Bluetooth) 、基站、软件版本号、网络接入方式、类型、状态、网络质量数据、操作、使用、服务日志、设备传感器数据。

+

b. 为了预防恶意程序、保障运营质量及效率,我们会收集安装的应用信息或正在运行的进程信息、应用程序的总体运行、使用情况与频率。应用崩溃情况、总体安装使用情况、性能数据、应用来源。

+

c. 为了分析应用新增、激活、留存、性能等统计性指标,我们可能会调用剪切板对相关统计信息进行归因分析,请您放心,我们不会收集您的隐私信息。

+

d. 我们可能使用您的账户信息、设备信息、服务日志信息以及我们关联方、合作方在获得您授权或依法可以提供的信息,用于判断账户安全、进行身份验证、识别违法违规情况、检测及防范安全事件,并依法采取必要的记录、分析、处置措施。

+

1.10 收集、使用个人信息目的变更

+

请您了解,随着我们业务的发展,可能会对 Voitrans + 的功能和提供的服务有所调整变化。原则上,当新功能或服务与我们当前提供的功能或服务相关时,收集与使用的个人信息将与原处理目的具有直接或合理关联。在与原处理目的无直接或合理关联的场景下,我们收集、使用您的个人信息,会再次按照法律法规及国家标准的要求以页面提示、交互流程、协议确认方式另行向您进行告知说明,并征得您的同意。 +

+

1.11 征得授权同意的例外

+

请您理解,在下列情形中,根据法律法规及相关国家标准,我们收集和使用您的个人信息不必事先征得您的授权同意:

+

a. 与我们履行法律法规规定的义务相关的;

+

b. 与国家安全、国防安全直接相关的;

+

c. 与公共安全、公共卫生、重大公共利益直接相关的;

+

d. 与刑事侦查、起诉、审判和判决执行等直接相关的;

+

e. 出于维护您或他人的生命、财产等重大合法权益但又很难得到本人授权同意的;

+

f. 您自行向社会公众公开的个人信息;

+

g. 根据个人信息主体要求签订和履行合同所必需的;

+

h. 从合法公开披露的信息中收集的您的个人信息的,如合法的新闻报道、政府信息公开等渠道;

+

i. 用于维护软件及相关服务的安全稳定运行所必需的,例如发现、处置软件及相关服务的故障;

+

j. 为开展合法的新闻报道所必需的;

+

k. 为学术研究机构,基于公共利益开展统计或学术研究所必要,且对外提供学术研究或描述的结果时,对结果中所包含的个人信息进行去标识化处理的;

+

l. 法律法规规定的其他情形。

+

特别提示您注意,如信息无法单独或结合其他信息识别到您的个人身份,其不属于法律意义上您的个人信息 ; + 当您的信息可以单独或结合其他信息识别到您的个人身份时或我们将无法与任何特定个人信息建立联系的数据与其他您的个人信息结合使用时,这些信息在结合使用期间,将作为您的个人信息按照本隐私政策处理与保护。

+

1.12 人机交互对话

+

在您使用我们的人机交互服务时(包括智能助手,等),我们会收集您主动输入的内容(包括文本、图片、音视频等),并基于这些信息为您提供相应的服务。当您使用语音输入时,我们将收集您的音频数据,进行语音转写,并基于转写后的文本生成对话内容,以提供更好的互动体验。 +

+

如果您输入的信息包含他人个人数据,请确保已获得他人的合法授权,避免不当泄露他人隐私。

+

为了更好地理解您的需求和语境,我们会对您提供的信息进行分析和处理,以便输出更具相关性的内容。在确保信息经过加密、安全处理且去标识化的前提下,我们可能会将所收集的数据和生成的回复用于模型训练,持续优化服务质量。

+

1.13 数据埋点统计及上报

+

我们在 APP 内设置了页面埋点,目的是为了统计 APP 的使用情况,以便更好地优化产品性能、修复漏洞以及提升您的使用体验。当您访问 APP + 页面时,页面埋点会收集您的页面浏览记录、点击行为、停留时长等信息,但此类信息的收集频率遵循合理必要原则,通常情况下,同一页面在您单次访问期间仅收集一次,不会对您造成频繁的数据收集干扰。仅在您进行交互操作(如点击按钮、切换页面板块等)触发新的业务需求时,才会根据实际需要再次收集与之相关的少量信息。 +

+

a. 我们收集的信息将主要用于为您提供、优化和改进本 APP 的服务,依据您的反馈修复程序问题等。

+

b. 部分信息可能会用于数据分析和研究,以了解用户群体的使用习惯和偏好趋势,从而推动本 APP 的持续创新发展。

+

1.14 模型服务与数据处理

+

1.14.1 模型服务基础

+

Voitrans 接入了目前国内比较优秀的的人工智能大模型服务,如豆包, DeepSeek,未来会持续优化模型的接入能力,拓展更多的模型,以为用户提供更好的服务。

+

1.14.2 数据收集与共享

+

在您使用第三方模型期间,为实现高效的聊天互动、回答问题、生成图像等服务,我们会收集您的输入内容并共享给相关服务提供方,以便根据您的内容提供文生文或文生图的人机交互服务。

+

1.14.3 模型服务说明

+

在您使用第三方模型期间,如遇到问题,可及时于我们取得联系,我们将持续优化模型的接入。

+

1.15 应用自启动与关联启动说明

+

1.15.1 自启动用途说明

+

Voitrans app 需要使用自启动能力以确保与外部蓝牙设备(如ble蓝牙耳机)保持稳定连接,这对于提供持续的语音交互和设备控制服务是必要的。自启动功能仅用于维持蓝牙连接服务,不会进行其他后台操作。

+

1.15.2 自启动场景说明

+
    +
  • 蓝牙设备连接:当配对的蓝牙设备在范围内被激活时,应用需要自动启动以建立连接。
  • +
  • 设备通信维护:为确保与蓝牙设备的实时通信质量,需要保持后台服务运行。
  • +
  • 语音交互响应:当用户通过蓝牙设备发起语音指令时,应用需要及时响应。
  • +
  • 设备状态监控:实时监控蓝牙设备的连接状态,确保服务的可靠性。
  • +
+

2. 数据使用过程中涉及的合作方以及转移、公开个人信息

+

2.1数据使用过程中涉及的合作方

+

2.1.1原则

+

a.合法原则 :与合作方合作过程中涉及数据使用活动的,必须具有合法目的、符合法定的合法性基础。如果合作方使用信息不再符合合法原则,则其不应再使用您的个人信息,或在获得相应合法性基础后再行使用。

+

b.正当与最小必要原则 :数据使用必须具有正当目的,且应以达成目的必要为限。

+

c.安全审慎原则 :我们将审慎评估合作方使用数据的目的,对这些合作方的安全保障能力进行综合评估,并要求其遵循合作法律协议。我们会对合作方获取信息的软件工具开发包(SDK)、应用程序接口 (API) + 进行严格的安全监测,以保护数据安全。

+

2.1.2委托处理

+

对于委托处理个人信息的场景,我们会与受托合作方根据法律规定签署相关处理协议,并对其个人信息使用活动进行监督。

+

2.1.3共同处理

+

对于共同处理个人信息的场景,我们会与合作方根据法律规定签署相关协议并约定各自的权利和义务,确保在使用相关个人信息的过程中遵守法律的相关规定、保护数据安全。

+

2.1.4合作方的范围

+

若具体功能和场景中涉及由我们的关联方、第三方提供服务,则合作方范围包括我们的关联方与第三方。

+

2.1.5实现功能或服务的数据使用

+

a.地理位置服务 : 当您使用地理位置相关服务时,位置服务提供商 ( 高德地图 ) 可能会通过 SDK 或相关技术使用您的 GPS + 信息、设备信息以便可以向您返回位置结果。拒绝上述使用仅会影响地理位置服务功能,但不影响其他功能的正常使用。

+

b.客户服务 : 为及时处理您的投诉、建议以及其他诉求,客户服务提供商需要使用您的账号及所涉及事件的相关信息,以及时了解、处理和响应相关问题。

+

2.1.6实现安全与分析统计的数据使用

+

a.保障使用安全 : 我们非常重视账号、服务及内容安全,为保障您和其他用户的账号与财产安全,使您和我们的正当合法权益免受不法侵害,我们的合作方可能会使用必要的设备、账号及日志信息。

+

b.分析产品情况 : 为分析我们产品的使用和表现情况,我们的合作方可能需要使用该产品使用情况(崩溃、闪退 ) 、设备标识信息、应用总体安装使用情况等信息。

+

c. 为提升相关领域的科研能力,促进科技发展水平,我们在确保数据安全与目的正当的前提下,可能会与合作的科研院所、高校等机构使用去标识化或匿名化的数据。

+

2.2 转移

+

随着我们业务的持续发展,我们将有可能进行合并、收购、资产转让,您的个人信息有可能因此而被转移。在发生前述变更时,我们将按照法律法规及不低于本隐私政策所载明的安全标准要求继受方继续履行个人信息处理者的义务,若继受方变更原先的处理目的、处理方式的,我们将要求继受方重新征得您的授权同意。 +

+

2.3 公开

+

a. 我们不会公开您的信息,除非遵循国家法律法规规定或者获得您的同意。我们公开您的个人信息会采用符合行业内标准的安全保护措施。

+

b.对违规 账号 、欺诈行为进行处罚公告时,我们会披露相关账号的信息。

+

2.4 依法豁免征得同意提供、公开的个人信息

+

请您理解,在下列情形中,根据法律法规及国家标准,我们向合作方提供、公开您的个人信息无需征得您的授权同意 :

+

a. 为订立、履行您作为一方当事人的合同所必需,或者按照依法制定的劳动规章制度和依法签订的集体合同实施人力资源管理所必需;

+

b.为履行法定职责或者法定义务所必需 ;

+

c.与国家安全、国防安全直接相关的 ;

+

d.与刑事侦查、起诉、审判和判决执行等直接相关的 ;

+

e.为应对突发公共卫生事件,或者紧急情况下为保护自然人的生命健康和财产安全所必需 ;

+

f. 出于维护您或其他个人的生命、财产等重大合法权益但又很难得到您本人授权同意的;

+

g.为公共利益实施新闻报道、舆论监督等行为 , 在合理的范围内处理个人信息 ;

+

h.依照相关法律规定处理您自行公开或者其他已经合法公开的个人信息 ;

+

i.从合法公开披露的信息中收集个人信息的,如合法的新闻报道、政府信息公开等渠道;

+

j.法律、行政法规规定的其他情形。

+

2.5 支付

+

您授权我们通过指定的付款方式向您收取费用。当您使用Voitrans + app及相关服务涉及交易时,系统将会生成您的交易订单,您需要填写购买人姓名、订单相关手机号码,订单会自动生成您所购买的商品或服务信息、您应支付的货款金额及您选择的支付方式等支付信息,您可能在订单上补充告知相关方的其他信息以方便交易的实现。 +

+

当您使用涉及支付的功能时:支付通过合作的第三方支付机构(微信支付苹果支付等)所提供的支付服务完成,第三方支付服务有可能使用生物识别信息用于付款验证,您在使用第三方支付服务的时候请阅读该第三方机构的用户协议、隐私政策,如对于该第三方机构的用户协议、隐私政策不同意的,请您及时停止使用该第三方机构的支付服务。 +

+

3. 我们如何存储个人信息

+

3.1存储地点

+

我们依照法律法规的规定,将在境内运营过程中收集和产生的您的个人信息 存储于中华人民共和国境内。目前,我们不会将上述信息传输至境外,如果我们向境外传输,会严格遵守中国的相关法律、监管政策,并会遵循相关国家规定或者征求您的同意。 +

+

3.2 存储期限

+

3.2.1 我们仅在为提供Voitrans 服务之目的所必需的期间内保留您的个人信息,例如:

+

a. 手机号码: 当您使用手机号码注册登录时,我们需要持续保留您的手机号码,以便于向您提供正常的服务、应对您可能的查询和客诉并保障您的账号和系统安全。

+

b.电子邮箱:当您使用电子邮箱注册登录时,我们需要持续保留您的电子邮箱,以便于向您提供正常的服务、应对您可能的查询和客诉并保障您的账号和系统安全。

+

c.第三方授权:当您使用其他合作方授权方式登录时,我们会保留您的授权信息,以便您可以持续使用第三方登录方式进行登录,使得我们可以持续为您提供服务。

+

c. 您发布的信息、评论、点赞及相关信息,在您未撤回、删除或未注销账号期间,我们会保留相关信息用于向您提供持续的业务功能。

+

d. 根据《中华人民共和国网络安全法》、《中华人民共和国电子商务法》相关规定保留您的个人信息。

+

3.2.2 + 如果您注销账号、主动删除个人信息或超出必要的期限后,我们将对您的个人信息进行删除或匿名化处理,但以下情况除外:

+

a .遵从法律法规有关信息留存的要求;

+

b .遵守法院判决、裁定或其他法律程序的规定;

+

c. 遵守相关政府机关或法定授权组织的要求;

+

d. 出于财务、审计、争议解决等目的需要合理延长期限的。

+

4. 我们如何保护个人信息的安全

+

4.1 我们非常重视您个人信息的安全,将努力采取合理的安全措施 ( 包括技术方面和管理方面 ) 来保护您的个人信息,防止您提供的个人信息被不当使用或未经授权的情况下被访问、公开披露、使用、修改、损坏、丢失或泄漏。

+

4.2 我们会使用不低于行业同行的加密技术、 匿 名化处理及相关合理可行的手段保护您的个人信息,并使用安全保护机制防止您的个人信息遭到恶意攻击。

+

4.3 我们会建立专门的安全部门、安全管理制度、数据安全流程保障您的个人信息安全。我们采取严格的数据使用和访问制度,确保只有授权人员才可访问您的个人信息,并适时对数据和技术进行安全审计。

+

4.4 + 尽管已经采取了上述合理有效措施,并已经遵守了相关法律规定要求的标准,但请您理解,由于技术的限制以及可能存在的各种恶意手段,在互联网行业,即便竭尽所能加强安全措施,也不可能始终保证信息百分之百的安全,我们将尽力确保您提供给我们的个人信息的安全性。 +

+

4.5 您知悉并理解,您接入我们的服务所用的系统和通讯网络,有可能因我们可控范围外的因素而出现问题。因此,我们强烈建议您采取积极措施保护个人信息的安全,包括但不限于使用复杂密码、定期修改密码、不将自己的 账号 + 密码及相关个人信息透露给他人。

+

4.6 我们会制定应急处理预案,并在发生用户信息安全事件时立即启动应急预案,努力阻止这些安全事件的影响和后果扩大。一 旦 发生用户信息安全事件(泄露、丢失 ) 后,我们将按照法律法规的要求,及时向您告知 : + 安全事件的基本情况和可能的影响,我们已经采取或将要采取的处置措施、您可自主防范和降低风险的建议、对您的补救措施。我们将及时将事件相关情况以推送通知、邮件、信函、短信及相关形式告知您,难以逐一告知时,我们会采取合理、有效的方式发布公告。同时,我们还将按照相关监管部门要求,上报用户信息安全事件的处置情况。 +

+

4.7 您一旦离开Voitrans 平台及相关服务,浏览或使用其他网站、服务及内容资源,我们将没有能力和直接义务保护您在Voitrans + 平台及相关服务之外的软件、网站提交的任何个人信息,无论您登录、浏览或使用上述软件、网站是否基于Voitrans 平台的链接或引导。

+

5. 我们如何管理您的个人信息

+

我们非常重视您对个人信息的管理,并尽全力保护您对于您个人信息的查阅、复制、更正、补充、删除、撤回同意授权、注销账号、投诉举报以及设置隐私功能等权利,以使您有能力保障您的隐私和信息安全。

+

5.1 改变或撤回授权范围

+

5.1.1 改变或撤回敏感权限设置 : + 特定的业务功能和服务将需要您的设备敏感权限才能得以完成,当您改变或撤回敏感权限后,我们无法继续为您提供撤回敏感权限所对应的功能和服务,也不再处理您相应的敏感权限信息。但您撤回敏感权限的决定,不会影响我们此前基于您的授权而开展的个人信息处理。您可以通过 + 更改移动终端的设置 开启或关闭敏感权限。

+

5.1.2 改变或撤回授权的信息处理 : + 特定的业务功能和服务将需要您的信息才能得以完成,当您撤回同意或授权后,我们无法继续为您提供撤回同意或授权所对应的功能和服务,也不再处理您相应的个人信息。但您撤回同意或授权的决定,不会影响我们此前基于您的授权而开展的个人信息处理。 +

+

5.2 您的个人信息权利

+

5.2.1 查阅个人信息 :

+

您可以通过Voitrans APP【个人中心】查阅您的昵称、手机号等个人信息。

+

5.2.2 更正个人信息 :

+

您可以通过Voitrans APP【个人中心】修改您的昵称 等个人信息 。

+

特别提示您注意,出于安全性和身份识别(如号码申诉服务 ) 的考虑,您可能无法自主修改注册时提交的某些初始注册信息。如您确有需要修改该类注册信息,请根据本隐私政策载明的联系方式联系我们。

+

除上述外,如果您或其他有权主体对相关法律法规赋予的个人信息权利的行使,有任何主张,要求或者疑问,您可以通过我们预留的联系方式(官网、APP内)与我们联系,我们将尽快审核所涉问题,并在验证您的用户身份后的1 0 个工作日内回复。 +

+

5.3 注销 账号

+

您可以通过Voitrans APP【设置】 -【个人中心】 - 【注销 账号 】与我们联系申请注销您的账号,我们将会在收到您的申请后及时进行处理,并在1 0 + 个工作日或法律法规要求的时限内回复您的请求。在您注销账号前,我们将验证您的个人身份、安全状态、设备信息等。您知悉并理解,注销账号是不可逆的行为。

+

5.4 意见反馈和投诉举报

+

您可以按照我们公示的制度进行投诉或举报。如果您有任何意见或您认为您的个人信息权利可能受到侵害,或者发现侵害个人信息权利的线索,您可以通过Voitrans APP【设置】 - 【用户反馈】与我们联系。

+

5.5 访问隐私政策

+

您可以在Voitrans平台的注册页面,或者在Voitrans APP【设置】 -【个人中心】 - 【 隐私政策 】查看本隐私政策。

+

请您了解,本隐私政策中所述的 Voitrans及相关服务可能会根据您所使用的手机型号、系统版本、软件应用程序版本、移动客户端等因素而有所不同。最终的产品和服务以您所使用的Voitrans 软件及相关服务为准。

+

6. 停止运营并向您告知

+

如我们停止运营,我们将及时停止收集您个人信息的活动,将停止运营的通知以逐一送达或公告的形式通知您,并对所持有的您的个人信息进行删除或匿名化处理。

+

6.1 未成年人通用条款

+

a. 若您是已满14周岁但未满 18 周岁的未成年人,在使用Voitrans平台及相关服务前,应在您的父母或其他监护人监护、指导下共同阅读并同意本隐私政策。

+

b. 若您是未满 14 周岁的未成年人的监护人,在使用Voitrans 平台及相关服务前,应由您代被监护人阅读并同意本隐私政策。

+

c. 我们根据国家相关法律法规的规定保护未成年人的个人信息,只会在法律允许、父母或其他监护人明确同意或保护未成年人所必要的情况下收集、使用或披露未成年人的个人信息 ; + 如果我们发现在未事先获得可证实的父母或其他监护人同意的情况下收集了未成年人的个人信息,则会设法尽快删除相关信息。同时我们建立了严格的儿童信息收集使用规则,以保护儿童个人信息安全。

+

d. 若您是未成年人的监护人,当您对您所监护的未成年人的个人信息有相关疑问时,请通过本隐私政策公示的联系方式与我们联系。

+

7. 隐私政策的修订和通知

+

7.1 为了给您提供更好的服务,Voitrans + 平台及相关服务将不时更新与变化,我们会适时对本隐私政策进行修订,这些修订构成本隐私政策的一部分并具有等同于本隐私政策的效力,未经您明确同意,我们不会削减您依据当前生效的本隐私政策所应享受的权利。

+

7.2 本隐私政策更新后,我们会在Voitrans 平台发出更新版本,并在更新后的条款生效前通过官方系统公告或其他适当的方式提醒您更新的内容,以便您及时了解本隐私政策的最新版本。

+

8. 联系我们

+

8.1 如果您对个人信息保护问题有投诉、建议、疑问,您可以通过Voitrans APP【设置】 - 【用户反馈】与我们联系,我们核查并验证您的用户身份后会及时给您反馈,一般情况下我们将在10个工作日内回复您的请求。

+

8.2 如对本隐私政策内容有任何疑问、意见或建议,您可以通过Voitrans APP【设置】 - 【用户反馈】与我们联系。

+

9. 第三方信息共享清单

+

以下为我们接入的第三方SDK名称、开发者信息及其隐私政策链接说明,便于您更清楚地了解您的个人信息使用与保护情况:

+

9.1 微信OpenSDK Android

+

信息获取:设备号、IP地址、设备软件信息、设备识别码(IMEI、Android ID、IDFA、OAID、OPENUDID、GUID、ICCID、SIM 卡 IMSI + 信息、IP、MAC地址)、设备识别符、所在地区、设备相关应用信息以及其他与服务相关的日志信息

+

使用目的:微信登录与分享,详情见SDK隐私政策

+

使用场景:在用户使用微信登录与分享时使用,详情见SDK隐私政策

+

合作方主体:深圳市腾讯计算机系统有限公司

+

收集方式:SDK自行采集

+

隐私政策链接:https://support.weixin.qq.com/cgi-bin/mmsupportacctnodeweb-bin/pages/RYiYJkLOrQwu0nb8

+

9.2 导航SDK

+

信息获取:应用名、应用版本号、设备品牌及型号、操作系统、运营商信息、屏幕分辨率、传感器信息(矢量、加速度、压力、方向、地磁 、陀螺、温度、光敏、nema信号)、设备信号强度信息

+

使用目的:使用高德地图进行导航

+

使用场景:使用高德地图进行导航

+

合作方主体:北京高德图强科技有限公司

+

收集方式:SDK自行采集

+

隐私政策链接:https://lbs.amap.com/pages/privacy/

+

9.3 地图SDK

+

信息获取:应用名、应用版本号、设备品牌及型号、操作系统、网络信息、设备信号强度信息等

+

使用目的:地图展示与交互

+

使用场景:地图相关功能

+

合作方主体:北京高德图强科技有限公司

+

收集方式:SDK自行采集

+

隐私政策链接:https://lbs.amap.com/pages/privacy/

+

9.4 定位SDK

+

信息获取:位置信息(GPS、Wi-Fi、基站)、设备信息、网络信息等

+

使用目的:提供定位服务

+

使用场景:位置相关功能

+

合作方主体:北京高德图强科技有限公司

+

收集方式:SDK自行采集

+

隐私政策链接:https://lbs.amap.com/pages/privacy/

+

9.5 轻量版地图SDK

+

信息获取:应用名、应用版本号、设备信息、网络信息等

+

使用目的:地图工具与渲染优化

+

使用场景:地图相关工具功能

+

合作方主体:北京高德图强科技有限公司

+

收集方式:SDK自行采集

+

隐私政策链接:https://lbs.amap.com/pages/privacy/

+

9.6 APP支付客户端SDK

+

信息获取:设备标识、设备信息、软件安装列表、网络信息等(详见官方隐私政策)

+

使用目的:支付宝账号登录与调起支付能力

+

使用场景:支付相关功能

+

合作方主体:支付宝(杭州)信息技术有限公司

+

收集方式:SDK自行采集

+

隐私政策链接:https://opendocs.alipay.com/open/54/01g6qm#%E6%94%AF%E4%BB%98%E5%AE%9D%20App%20%E6%94%AF%E4%BB%98%E5%AE%A2%E6%88%B7%E7%AB%AF%20SDK%20%E9%9A%90%E7%A7%81%E6%94%BF%E7%AD%96

+

9.7 灯塔 SDK Android

+

信息获取:SDK或API版本、应用标识符、应用版本、终端厂商、操作系统版本、语言、时区、网络状态、崩溃与使用日志、IP地址等(详见官方隐私政策)

+

使用目的:应用质量与数据上报服务

+

使用场景:稳定性监测与性能分析

+

合作方主体:深圳市腾讯计算机系统有限公司

+

收集方式:SDK自行采集

+

隐私政策链接:https://privacy.qq.com/document/preview/a19a8cf4f2354306b137a7db39d32024

+ + + diff --git a/doc/ysxy_en.html b/doc/ysxy_en.html new file mode 100644 index 000000000..8c51573d4 --- /dev/null +++ b/doc/ysxy_en.html @@ -0,0 +1,381 @@ + + + + + + + Voitrans Privacy Policy + + + + +
+

Voitrans Privacy Policy

+

Last Updated: May 27, 2025

+

Effective Date: May 27, 2025

+ +

Welcome to use Voitrans Software and Services

+

1. Voitrans Provided by Saitong Industries Inc. (hereinafter referred to as "the Company"). Can help users achieve Bluetooth devices and other products management. We understand the importance of personal information to our users (hereinafter referred to as "you") and that effective protection of your personal information is the cornerstone of the healthy and sustainable development of our business. Thank you for your use and trust in Voitrans products or services! We are fully aware that this trust does not come easily, we adhere to the principle of openness and transparency, to express to you our rules for processing your personal information, we use legal and legitimate ways and clear and reasonable purposes to process your personal information. Special note: Please be sure to read and understand this Privacy Policy carefully before using our products or services so that you can make appropriate choices.

+

2. This Privacy Policy explains to you how we collect, use, store and share your personal information when you use Voitrans. Personal information refers to all kinds of information recorded by electronic or other means that can identify a specific natural person or reflect the activities of a specific natural person alone or in combination with other information. Voitrans respects and protects the personal privacy of all users of the service. Except as otherwise provided in this Privacy Policy, Voitrans will not disclose this information to the public or provide it to third parties without your prior permission.

+

3. When you use a third party's product or service through an App Marketplace product or service, your personal information shall be subject to the third party's privacy policy.

+

4. Voitrans This Privacy Policy will be updated from time to time. By agreeing to the Voitrans Service Usage Agreement and beginning to use Voitrans, you are deemed to have agreed to all of this Privacy Policy. If you do not agree to this Privacy Policy, please do not use Voitrans Software and Services.

+

5. In order to analyze and measure the effectiveness of relevant services, we will use and disclose your personal information in accordance with the provisions of this Privacy Policy.

+ +

This Privacy policy will help you understand the following:

+
    +
  • 1 The information we collect
  • +
  • 2 Storage of information
  • +
  • 3 Information security
  • +
  • 4 How do we use the information we collect
  • +
  • 5 Information sharing
  • +
  • 6 How do you manage your personal information
  • +
  • 7 Application permission description
  • +
  • 8 Access third-party SDKS
  • +
  • 9 Protection of minors
  • +
  • 10 Effective and updated Privacy Policy
  • +
  • 11 How to contact us
  • +
+ +

1. The information we collect

+

1. Use Voitrans at the beginning We will start collecting data about your usage of the application

+

2. Voitrans may obtain personal information about you from third parties or business partners through lawful means

+

3. In order to provide you with better products and services, give you network security during use, maintain the normal operation of products and optimize and improve, the purpose of our information services, after you actively click to agree to this privacy policy, use the information we provide. During the service, in addition to the personal information necessary to ensure the normal operation of the basic functions of the App in accordance with national laws and regulations, we will collect other personal information from you:

+

3.1 In order to identify risk users, judge the authenticity of users and prevent cheating in an all-round way, we will read your device information every time you open the software, including: (Device model, device MAC address, device unique identifier (IMEI, AndroidID, Device ID, MEID, UUID, GAID, OAID, IDFA, OpenUDID, GUID, IMSI, CCID, etc.) MEID, serial number, SSID and BSSID of WIFI), read Bluetooth address, operating system version, operating system name, user login account and password information;

+

3.2 When you use our products and services, we may collect certain information through network log information or certain programs and software, such information may include: details of the use of products and services, such as the opening time of your application, the length of use, and the address of the page visited; Device event information, such as crashes, system activity, etc., to optimize your device and improve our services;

+

3.3 To identify applications where equipment is at risk, provide more secure functional services. Therefore, we need to obtain your application/application installation list in the process to ensure the normal operation of the product and protect the security of the device.

+

3.4 In order to analyze the use of application functions by users and determine the authenticity of users, we need to conduct data statistics, and collect the above device information, including device MAC address and device unique identifier, when your device is in use.

+

3.5 Device signal: the network status of the device you use, WIFI access point information, etc.

+

3.6 Location Information: We may use GPS, WiFi or other technical means to collect and process information about your location for updating local information, providing content services, etc.

+

For more information, please click "Personal Information Collection List".

+ +

1. For the more sensitive personal information in the above information, such as user device information (IMEI number, call status and identification number), GPS location information, photo media content and file access rights, we will obtain your consent before collection. For some non-sensitive information, the collection may be default and automatic. You can restrict our collection of relevant information or use of certain permissions from your mobile device by adjusting the Settings option for your mobile device. When you restrict our access to a certain permission, we may not be able to continue to provide you with the corresponding services, and no longer process your corresponding information.

+

2. We will not collect your ID card number, personal biometric information, bank account number, property information, credit information, health and physiological information and other personal sensitive information.

+

3. About sensitive permissions: In order to properly use the application functions, the product needs to obtain the following permissions in order to provide functional services related to normal products.

+ +

1 Obtain storage read and write permissions

+

1 How to get: pop-up prompt, when the permission is obtained, the user will take the initiative to click agree application will use this permission

+

2 Purpose and scope of use: It is used to scan the resource cache files and thumbnails in the system, load music resources and product pictures. It is displayed on the App interface to facilitate users to view and operate

+ +

2 Obtain permission to the application installation list

+

1 How to get: pop-up prompt, when the permission is obtained, the user will take the initiative to click agree application will use this permission

+

2 Purpose and scope of use: When the application needs to use advertising promotion services, the third-party advertising platform, Youmon +SDK, needs to obtain the "app software installation list permission" of the device to prevent the user from pushing the application already owned by the user and improve the user experience.

+ +

3 The permission to obtain device status information

+

Includes: (Device model, device MAC address, device unique identifier (IMEI, AndroidID, Device ID, MEID, UUID, GAID, OAID, IDFA, OpenUDID, GUID, IMSI, CCID, etc.) MEID, serial number, SSID and BSSID of WIFI, read Bluetooth address), operating system version, operating system name, etc.;

+

1 How to get: pop-up prompt, when the permission is obtained, the user will take the initiative to click agree application will use this permission

+

2 Purpose and Scope of use: When using our application, in order to analyze the user's use of application functions and judge the user's authenticity, we will synchronize the permission to third-party SDKS, including Autonavi SDK, Youmon +SDK, etc.

+ +

4 Access to location information

+

1 How to obtain: pop-up prompts, when the permission is obtained, the user will use this permission only when he actively clicks agree with the application.

+

2 Purpose and Scope of use: When the application needs to use GPS, WiFi or other functions, the product needs to collect your location and information to search for Bluetooth devices in nearby locations.

+

Note: For more application permissions, see this document "Privacy Policy" Section 7 "Application Permission Description"

+ +

2. Storage of information

+

1. Where the information is stored

+

The personal information we collect and generate in the People's Republic of China will be stored on our servers and/or those of our affiliates.

+

2. Information storage and processing

+

We store your personal information only for the period necessary to achieve the purpose or as long as required by law and regulation. After you stop using this product, we will immediately stop collecting and processing your personal information and application data. When the above period is exceeded, we will delete or anonymize the personal information you have provided to us.

+

In addition to the necessary functions, Voitrans needs to apply for corresponding system permissions. The server side of the cloud does not store user personal information, and the third-party SDK accessed will be anonymized and de-identified by the service provider (Amu Statistics SDK, Autonavi SDK), so users cannot be tracked. Information stored on the user's phone will be automatically deleted when the user uninstalls Voitrans or clears the cache.

+ +

3. Information security

+

1. Voitrans stores and protects your personal information using industry-standard security measures and technologies, such as authentication, data encryption, restricted data center access and other security measures to ensure that your information is secure from disclosure, alteration and destruction. Despite the aforementioned security measures, please note that no security measure can be 100% secure.

+

2. When you use the services provided by Voitrans, please protect your personal information. If you find that your personal information has been compromised, please contact us immediately so that we can take appropriate measures.

+ +

4. How do we use the information we collect

+

1. When implementing application business functions, we may use your personal information to ensure the integrity and security of the products and services we provide to you;

+

2. We will understand your usage habits through the collected information, and with the help of data analysis, identify the acceptability of services, your usage habits, etc., in order to test and create new products, functions and services, improve the service level, and help us improve existing services and develop new services;

+

3. We may use your personal information for customer surveys about the products and services, for sending notices about the products and services, and for commercial promotion;

+

4. We may use your information for internal analysis and research related to the application;

+ +

5. Information sharing

+

At present, we will not actively share or transfer your personal information to third parties outside the company, if there is other sharing or transfer of your personal information, we will directly or confirm that the third party has obtained your express consent to the above behavior.

+

We will not publicly disclose the personal information we collect. If it is necessary to publicly disclose it, we will inform you of the purpose of the public disclosure, the type of information disclosed and the sensitive information that may be involved, and obtain your express consent.

+

As our business continues to develop, we may engage in mergers, acquisitions, asset transfers and other transactions, and we will inform you of the relevant circumstances, in accordance with laws and regulations and no less than the standards required by this Guide to continue to protect your personal information or require a new controller to continue to protect your personal information.

+

In addition, according to relevant laws and regulations and national standards, we may share, transfer and publicly disclose personal information without prior authorization and consent of the personal information subject under the following circumstances:

+
    +
  • 1 Directly related to national security and national defense security;
  • +
  • 2 Directly related to public safety, public health or major public interests;
  • +
  • 3 Directly related to criminal investigation, prosecution, trial and execution of judgments;
  • +
  • 4 In order to protect the life, property and other major legitimate rights and interests of the personal data subject or other individuals, but it is difficult to obtain the consent of the person;
  • +
  • 5 Personal information disclosed to the public by the personal information subject;
  • +
  • 6 Collecting personal information from legally publicly disclosed information, such as legitimate news reports, government information disclosure and other channels.
  • +
+ +

6. How do you manage your personal information

+

In accordance with the relevant laws, regulations and standards of China, as well as the common practices of other countries and regions, we guarantee that you exercise the following rights to your personal information:

+ +

1. Correct your personal information

+

If you find that there is an error in your personal information processed by us, you have the right to correct or update the wrong or incomplete information. If you are unable to operate independently in the account center, you can email saitong.voitrans@gmail.com Contact us

+ +

2. Delete your personal information

+

In the following cases, you may make a request to us to delete your personal information:

+
    +
  • If we process personal information in violation of laws and regulations;
  • +
  • If we collect and use your personal information without your consent;
  • +
  • If our processing of personal information violates our agreement with you;
  • +
  • If you no longer use our products or services;
  • +
  • If we no longer provide you with products or services.
  • +
+

Contact us. We will process your request within saitong.voitrans@gmail.com Contact us. We will send it to you after receiving it 30 working days of receipt

+ +

3. Change the scope of your consent

+

You can contact us by email saitong.voitrans@gmail.com Contact us. At your request, we will no longer collect, use or disclose your personal information. Please note that your withdrawal of consent or deletion of your personal information may affect your use of some of our services.

+ +

4 Access and copy personal information

+

You may, in accordance with the provisions of the relevant laws, provide your imei (phone serial number) or oaid Or Android ID After the account information and other information, you can apply to us for access to and copy your personal information, you can email to 3514406195@qq.com Get in touch with us.

+ +

5. In response to your above request

+

For security purposes, you may need to provide a written request or otherwise prove your identity. We may ask you to verify your identity before processing your request. For your reasonable request, we will respond and deal with it in a timely manner. We may reject requests that are unnecessarily repetitive, require too much technology (for example, the development of new systems or fundamental changes to existing practices), pose a risk to the legitimate interests of others, or are highly impractical.

+

In the following circumstances, according to the requirements of laws and regulations, we will not be able to respond to your request:

+
    +
  • Related to national security and national defense security;
  • +
  • In connection with criminal investigation, prosecution and trial;
  • +
  • There is sufficient evidence that you have subjective malice or abuse of rights;
  • +
  • Responding to your request will cause serious damage to the legitimate rights and interests of you or other individuals and organizations.
  • +
+ +

6. Account cancellation

+

User can be in Perform [account cancellation] operation on the APP. Operation steps:

+
    +
  1. Go to the [My] page
  2. +
  3. Click the [Settings] button on the upper right
  4. +
  5. Click [Cancel account] to confirm
  6. +
+

After you log out, the information about the user account is cleared

+ +

7. Application permission description

+

In order to provide better products and services, when you use the software, we will apply for the following rights, when you enter the home page or use the function will request authorization, if you refuse, some of the corresponding functions will not be able to use normally. The specific purposes and reminders of permissions are described in detail as follows:

+ +

1 Location rights:

+

android.permission.ACCESS_COARSE_LOCATION

+

android.permission.ACCESS_FINE_LOCATION

+

pass WiFi or mobile base station to obtain the user's latitude and longitude location information

+

When you use In Voitrans, we will search for your attached Bluetooth device based on your geographical location. At the same time, in order to provide more stable services, optimize products and improve experience, we will share the above information with three SDKS, such as Youmon and Autonavi, to obtain Voitrans operating data and functional services.

+

Reminder mode: A prompt indicating the permission to start the application for the first time is displayed, requiring the user to confirm acceptance. User Agreement and Privacy Policy description.

+ +

2

+

android.permission.READ_EXTERNAL_STORAGE

+

android.permission.WRITE_EXTERNAL_STORAGE

+

This permission is required when you access an application to perform application management functions.

+

Reminder method: User Agreement and Privacy Policy description.

+ +

3 Network permissions:

+

android.permission.INTERNET

+

When you use These permissions are required for Voitrans's information services and version upgrade services, and in order to improve the user experience and optimize the service capabilities, third-party SDKS such as Friends will also use these permissions.

+

Reminder mode: User Agreement and Privacy Policy description.

+ +

4 Get network status permissions:

+

android.permission.ACCESS_NETWORK_STATE

+

android.permission.ACCESS_WIFI_STATE

+

It is used to obtain whether the current network connection is valid, determine whether the current 4G environment is , and whether to remind the network environment changes, thus affecting the loading of information content. This permission is applied for when you use Voitrans.

+

Changes in the state of the network affect the uploading and loading of data. Therefore, the access to network status permission will be shared with friends + and other platforms.

+

Reminder method: User Agreement and Privacy Policy description.

+ +

5 Get task list permissions:

+

android.permission.GET_TASKS

+

This permission is used to obtain the application list for the software management function. When you use the software management function, you need to read the applications installed on the mobile phone.

+

Reminder mode: User Agreement and Privacy Policy description.

+ +

6 Obtain MAC address permission

+

android.permission.ACCESS_WIFI_STATE

+

android.permission.INTERNET

+

android.permission.ACCESS_NETWORK_STATE

+

When you use Voitrans, the application will need to obtain MAC address permissions:

+

When an application needs to report target conversion data, third-party platforms such as Youmon +SDK need to use MAC.

+

Reminder method: User Agreement and Privacy Policy description.

+ +

7 Obtain permission to record audio

+

android.permission.RECORD_AUDIO

+

When you use the voice input function of Voitrans, you need to use the permission to record audio, and mainly use the voice input for translation and other related functions.

+

Reminder method: User Agreement and Privacy Policy description , the user dynamically pops the box when required.

+ +

8 Obtain SSID permission for WIFI

+

When this permission is used, the app reads the SSID permission for WIFI:

+

Scope of use and purpose: When the application needs to use data analysis services, third-party platforms such as +SDK need to read the device's "WIFI SSID permission" to provide users with more accurate and effective functional services.

+

Reminder method: User Agreement and Privacy Policy description.

+ +

9 Get Bluetooth access

+

When this permission is used, the app reads the Bluetooth permission:

+

Scope of use and purpose: Bluetooth devices for searching and configuring accessories, such as Bluetooth headsets, Bluetooth watches, etc. Easy for users to manage

+

Reminder method: A prompt indicating the permission to start the application for the first time is displayed, requiring the user to confirm acceptance. User Agreement and Privacy Policy description.

+ +

10 Get the BSSID permission for WiFi

+

When the application needs to distinguish and identify the user to provide functional services and data statistics, it will read the "WiFi BSSID" permission of the device for:

+

Scope of use and purpose: 1 Third-party platforms: such as Youmeng SDK, Bugly SDK, Autonavi SDK

+

Reminder method: User Agreement and Privacy Policy description.

+ +

11 Obtain the application installation permission

+

android.permission.REQUEST_INSTALL_PACKAGES

+

The software will use the application installation permission, which is mainly used to allow users to install a new version and experience new functions when there is a new version.

+

Reminder mode: User Agreement and Privacy Policy description.

+ +

12 Get permissions for the camera

+

android.permission.CAMERA

+

Voitrans uses camera permissions to scan QR codes and bind Bluetooth devices such as Bluetooth headsets and watches.

+

Reminder method: A prompt indicating the permission to start the application for the first time is displayed, requiring the user to confirm acceptance. User Agreement and Privacy Policy description.

+ +

Special note:

+

1 Application integration within their Allies SDK, Scott map SDK in the user agrees to allow after use, application in the background/foreground for ICCID, IMSI, IMEI, SN, ANDROID, BSSID, ANDROID ID, SSID, IMSI and other personal information;

+

Remarks: More Please see the following SDK-related content and permissions [VIII. Access to third-party SDKS list]

+ +

8. Access third-party SDKS

+

1 In order to ensure the stable operation and functional realization of our client, so that you can use and enjoy more services and functions, our application will be embedded in the software tool development kit (SDK) or other similar applications of third parties, these third parties will collect and process relevant information in accordance with their own privacy policies, in order to normally provide functions and advertising services. You can learn about the information collection and processing rules of the third party in the following section. We will conduct strict security tests on the development kits (SDKS) for partners to obtain information, in order to protect the reasonable use and security of your personal information. Please note that third-party SDKS may change the data processing types due to their version upgrades. Please refer to the privacy policies published by third-party SDKS. See the details below or click to view the Third Party SDK List.

+ +

8.1 WeChat OpenSDK Android (com.tencent.mm.opensdk)

+

Information Obtained: Device number, IP address, device software information, device identification code (IMEI, Android ID, IDFA, OAID, OPENUDID, GUID, ICCID, SIM card IMSI information, IP, MAC address), device identifier, region, device-related application information, and other service-related log information

+

Purpose of Use: WeChat login and sharing

+

Developer: Shenzhen Tencent Computer Systems Company Limited

+

Collection Method: SDK self-collection

+

Privacy Policy Link: https://support.weixin.qq.com/cgi-bin/mmsupportacctnodeweb-bin/pages/RYiYJkLOrQwu0nb8

+ +

8.2 Navigation SDK (com.amap.api.navi)

+

Information Obtained: Application name, application version number, device brand and model, operating system, carrier information, screen resolution, sensor information (vector, acceleration, pressure, direction, geomagnetic, gyroscope, temperature, light sensor, NMEA signal), device signal strength information

+

Purpose of Use: Navigation using Amap

+

Developer: Beijing Gaode Tuqiang Technology Co., Ltd.

+

Collection Method: SDK self-collection

+

Privacy Policy Link: https://lbs.amap.com/pages/privacy/

+ +

8.3 Map SDK (com.amap.api.maps)

+

Information Obtained: Application name, application version, device brand and model, operating system, network information, device signal strength, etc.

+

Purpose of Use: Map display and interaction

+

Developer: Beijing Gaode Tuqiang Technology Co., Ltd.

+

Collection Method: SDK self-collection

+

Privacy Policy Link: https://lbs.amap.com/pages/privacy/

+ +

8.4 Location SDK (com.amap.api.location)

+

Information Obtained: Location information (GPS, Wi-Fi, base station), device information, network information, etc.

+

Purpose of Use: Provide location services

+

Developer: Beijing Gaode Tuqiang Technology Co., Ltd.

+

Collection Method: SDK self-collection

+

Privacy Policy Link: https://lbs.amap.com/pages/privacy/

+ +

8.5 Lite Map SDK (com.amap.api.maps.utils)

+

Information Obtained: Application name, application version, device information, network information, etc.

+

Purpose of Use: Map tools and rendering optimization

+

Developer: Beijing Gaode Tuqiang Technology Co., Ltd.

+

Collection Method: SDK self-collection

+

Privacy Policy Link: https://lbs.amap.com/pages/privacy/

+ +

8.6 Alipay App Client SDK (com.alipay.sdk)

+

Information Obtained: Device identifiers, basic device information, installed software list, and network information (see official privacy policy for details)

+

Purpose of Use: Alipay account login and payment invocation

+

Developer: Alipay (Hangzhou) Information Technology Co., Ltd.

+

Collection Method: SDK self-collection

+

Privacy Policy Link: https://opendocs.alipay.com/open/54/01g6qm#%E6%94%AF%E4%BB%98%E5%AE%9D%20App%20%E6%94%AF%E4%BB%98%E5%AE%A2%E6%88%B7%E7%AB%AF%20SDK%20%E9%9A%90%E7%A7%81%E6%94%BF%E7%AD%96

+ +

8.7 Beacon SDK Android (com.tencent.beacon)

+

Information Obtained: SDK/API version, platform, timestamp, app identifier, app version, device manufacturer, OS version, locale, timezone, network status (Wi-Fi, etc.), network logs, crash and usage data, IP address, etc. (see official privacy policy for details)

+

Purpose of Use: App quality monitoring and data reporting service

+

Developer: Shenzhen Tencent Computer Systems Company Limited

+

Collection Method: SDK self-collection

+

Privacy Policy Link: https://privacy.qq.com/document/preview/a19a8cf4f2354306b137a7db39d32024

+ +

9. Protection of minors

+

Users of our products and services may include Minors under the age of 18, minor users should seek the advice of their parents or guardians regarding this Privacy Policy before using our products, and use our services or provide your information to us after obtaining the consent of and under the guidance of their parents or guardians. Guardians of minors who have questions may contact us through the contact details in this Privacy Policy.

+

If you do not agree to the collection of the aforementioned information by the above third-party service providers, please stop using the Software immediately. During your use of the Software, these third parties will collect and process relevant information in accordance with their own privacy policies to provide some functions, advertising and other services. If the information is leaked due to the collection of the aforesaid information by the relevant third-party service provider, the relevant third-party service provider shall bear the corresponding legal responsibility.

+ +

10. Effective and updated Privacy Policy

+

1. This Privacy policy is effective from the date of publication.

+

2. As our business evolves and changes, our Privacy Policy may change. We reserve the right to change our privacy policy at any time, so please check back often. If we make changes to our Privacy Policy, we will post these updates on our website, apps, and where we deem appropriate to keep you informed. If the privacy policy is updated, it will take effect from the date of update. Your continued use of our products or services means that you agree to our changed Privacy Policy.

+ +

11. How to contact us

+

Saitong Industries Inc Established on November 22, 2024, registered address: 18305 E Valley Blvd, Unit C, City of Industry, CA 91744, USA. If you want to know more about our company and platform, you can contact us through the contact information provided in this policy (saitong.voitrans@gmail.com).

+
+ + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7720e0d46..c2c5f976b 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -54,6 +54,15 @@ voitrans + + + CFBundleURLName + deepvoiceclient.payment + CFBundleURLSchemes + + deepvoiceclient + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) diff --git a/ios/Runner/RunnerProfile.entitlements b/ios/Runner/RunnerProfile.entitlements new file mode 100644 index 000000000..0c67376eb --- /dev/null +++ b/ios/Runner/RunnerProfile.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 38f439e9d..2870209ff 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -1,3 +1,4 @@ +import '../../data/services/meeting/summary_task_service.dart'; import '/data/services/pay_service.dart'; import '/data/services/qq_music.dart'; @@ -10,6 +11,8 @@ import '../../../data/services/ast_service.dart'; import '../../../data/services/speech_impl/azure_ast_service.dart'; import 'package:get/get.dart'; import '../../data/services/meeting/meeting_task_service.dart'; +import '../../data/services/meeting/meeting_task_service2.dart'; +import '../../data/services/meeting/meeting_task_aggregator.dart'; import '../../data/services/meeting/meeting_upload_service.dart'; import '../../data/services/novice_guide_service.dart'; import '../../data/services/speech_factory.dart'; @@ -69,6 +72,14 @@ class InitialBinding extends Bindings { // 会议助手任务服务 Get.lazyPut(() => MeetingTaskService(), fenix: true); + // 会议助手任务服务(极速识别版,2小时内音频) + Get.lazyPut(() => MeetingTaskService2(), fenix: true); + // 会议助手任务服务聚合器(统一列表与状态) + Get.lazyPut(() => MeetingTaskAggregator(), + fenix: true); + + // 会议助手总结任务服务 + Get.lazyPut(() => SummaryTaskService(), fenix: true); // 会议助手音频上传 Get.lazyPut(() => MeetingUploadService(), diff --git a/lib/core/translations/language/ar_sa.dart b/lib/core/translations/language/ar_sa.dart index d8a24f8f8..5208c374b 100644 --- a/lib/core/translations/language/ar_sa.dart +++ b/lib/core/translations/language/ar_sa.dart @@ -134,7 +134,8 @@ const Map arSA = { "initializing": "جاري التهيئة...", "bluetoothInitializationFailure": "فشل تهيئة خدمة البلوتوث", "checkingPermissions": "جاري التحقق من الأذونات...", - "lackPermissions": "أذونات البلوتوث مفقودة", + "lackPermissions": + "يرجى включить أذونات البلوتوث y الإمكانية الوصول إلى الموقع للبحث عن الأجهزة المحاورة", "searchingDevice": "جاري البحث عن الجهاز...", "findConnectedDevice": "تم العثور على جهاز متصل", "notFound": "لم يتم العثور على أجهزة بلوتوث متصلة", @@ -179,21 +180,9 @@ const Map arSA = { "conferenceAssistant": "مساعد اجتماعات الأعمال", "conferenceAssistantInfo": "تسجيل محاضر الاجتماعات في الوقت الفعلي، ترجمة متعددة اللغات", - "studyAssistant": "مساعد تعلم اللغة", - "studyAssistantInfo": "تصحيح النطق، ممارسة المحادثة في الوقت الفعلي", - "travelTranslator": "مساعد ترجمة السفر", - "travelTranslatorInfo": "ترجمة صوتية فورية، مقدمة عن مناطق الجذب السياحي", - "beSearching": "جاري البحث:", - "noAssistantFound": "لم يتم العثور على مساعد الذكاء الاصطناعي المقابل", + "usageStats": "إحصائيات الاستخدام", // 用量统计 + "usageStatsInfo": "عرض إحصائيات استخدامك", // 查看您的使用统计数据 "scene": "مشهد", - "youHaveSelected": "لقد اخترت:", - "newFunction": "وظيفة جديدة", - "youChoseTry": "لقد اخترت التجربة", - "aiSinger": "وضع مغني الذكاء الاصطناعي", - "aiSingerInfo": - "غنِ أي أغنية بصوت فنانك المفضل، حتى الأغاني التي كتبتها بنفسك", - "popularFunction": "مشاهد الوظائف الشائعة", - "tryNow": "جرب الآن", // 商务会议助手页 "startMeeting": "بدء الاجتماع", @@ -428,8 +417,12 @@ const Map arSA = { 'inputAttendees': 'أدخل الحضور', // 设置页 - "systemSettings": "إعدادات النظام", - "premiumMember": "عضو مميز", + "systemSettings": "إعدادات النظام", //系统设置 + "vipRecharge": "إعادة صيغ العضوية", //会员充值 + "aiRecharge": "عضو مميز", //AI充值 + "translationRecharge": "إعادة صيغ الترجمة", //翻译充值 + "meetingAssistantRecharge": "إعادة صيغ الاجتماع", //会议助手充值 + "cloudSync": "إعادة صيغ مزامنة السحابة", //云端同步 "joinDevice": "الجهاز المتصل:", "accountId": "معرف الحساب:", "systemSetting": "إعدادات النظام", @@ -505,7 +498,7 @@ const Map arSA = { //Agent 模块 // agent View - "XiaoYan": "شياو يان", + "Lily": "Lily", "clearChatHistory": "مسح سجل الدردشة", "online": "متصل", "inputMessage": "أدخل رسالة...", @@ -1159,4 +1152,111 @@ const Map arSA = { "justNow": "الآن", "minutesAgo": "منذ @minutes دقيقة", "yesterday": "أمس", + // Guide overlay keys (placeholders in English) + "sayLilyHint": "Say \"Lily Lily\" to the mic", + "imHere": "I’m here~", + "guideWelcomeWakeAI": "Welcome to Voitrans! First, wake your AI “Lily”.", + "aiReplyCompleted": "Lily has finished the reply!", + "aiThinking": "Lily is thinking…", + "guideAwakenSuccess": "Great! Lily is awake—ask it to help.", + "confirmInput": "Confirm Input", + "guideClickSendPrefix": "Tap send, ", + "guideWeatherStart": "Tell Lily: “What’s the weather today?”", + "guideWeatherInput": "What’s the weather today?", + "guideWeatherSend": "Lily will report the weather in real time", + "guideWeatherEnd": "Lily can report weather live—try other commands", + "guideMusicStart": "Tell Lily: “Play some rock music”", + "guideMusicInput": "Play some rock music", + "guideMusicSend": "Lily will find music for you", + "guideMusicEnd": "Lily found music—try other commands", + "guideNavStart": "Tell Lily: “Navigate to Shenzhen Airport”", + "guideNavInput": "Navigate to Shenzhen Airport", + "guideNavSend": "Lily will plan the route", + "guideNavEnd": "Navigation set—try other commands", + "guideMeetingStart": + "Tell Lily: “Create a meeting reminder for 3pm tomorrow”", + "guideMeetingInput": "Create a meeting reminder for 3pm tomorrow", + "guideMeetingSend": "Lily will create a meeting reminder", + "guideMeetingEnd": "Reminder created—try other commands", + "guidePhoneStart": "Tell Lily: “Call Mom”", + "guidePhoneInput": "Call Mom", + "guidePhoneSend": "Lily will make the call", + "guidePhoneEnd": "Call placed—try other commands", + "guideSmsStart": "Tell Lily: “Send Dad a text: I’ll be home for dinner”", + "guideSmsInput": "Text Dad: I’ll be home for dinner", + "guideSmsSend": "Lily will send the message", + "guideSmsEnd": "Message sent—voice control is convenient", + // Goods pages common footer (placeholders) + 'appNameProvider': 'App: VoiTrans · Provided by: VoiTrans', + 'copyrightVoitrans': '© 2025 Voitrans Technologies', + + // Meeting goods page (placeholders) + 'meetingHeaderDesc1': + 'Audio recording, transcription, keyword search, and auto summary.', + 'meetingHeaderDesc2': + 'Suitable for business meetings, class notes, and interview minutes.', + 'meetingAutoRecording': 'Auto recording and segmentation', + 'meetingSpeakerTagging': 'Speaker labeling', + 'meetingLongAudioTranscription': 'Long audio transcription', + 'meetingKeyPointSummary': 'Key point extraction', + 'meetingTopicNameSearch': 'Search by topic/name', + 'meetingTimelineJump': 'Timeline jump', + 'meetingExportSummary': 'Export minutes', + 'meetingMultiPlatformShare': 'Share across platforms', + 'meetingDisclaimer': + 'Use in quiet environments; performance depends on microphone and network.', + + // Translation goods page (placeholders) + 'transHeaderDesc1': + 'ترجمة متزامنة فورية وترجمة نصية مع بث صوتي منخفض التأخير.', + 'transHeaderDesc2': 'مناسب للسفر والأعمال والخطب والتدريس.', + 'translateZhEn': 'ترجمة صينية-إنجليزية', + 'lowLatencyVoiceBroadcast': 'بث صوتي منخفض التأخير', + 'multiLangTextTranslation': 'ترجمة نصية متعددة اللغات', + 'termOptimizationContextPreserve': 'تحسين المصطلحات والحفاظ على السياق', + 'lexiconTourBusinessLife': 'قواميس السفر/الأعمال/الحياة', + 'personalVocabularyMemory': 'ذاكرة المفردات الشخصية', + 'conversationSubtitleLargeText': 'عناوين المحادثة / وضع النص الكبير', + 'colorHighContrast': 'تباين ألوان عالٍ', + 'transDisclaimer': + 'تعتمد جودة الترجمة على الميكروفون وزمن الشبكة والضوضاء المحيطة.', + + // VIP goods page (placeholders) + 'benefitsOverview': 'نظرة عامة على المزايا', + 'higherQuota': 'حصة استخدام أعلى', + 'lowerUnitPrice': 'سعر أقل لكل وحدة', + 'priorityService': 'خدمة ذات أولوية', + 'exclusiveSupportChannel': 'قناة دعم مخصصة', + 'fastResponse': 'استجابة سريعة للمشكلات', + 'eventsAndDiscounts': 'فعاليات وخصومات', + 'memberExclusiveEvents': 'فعاليات حصرية للأعضاء', + 'longTermDiscountPlan': 'خطة خصم طويلة الأجل', + 'generalPrivileges': 'امتيازات عامة', + 'earlyAccessNewFeatures': 'وصول مبكر إلى الميزات الجديدة', + 'crossDeviceSync': 'مزامنة عبر الأجهزة', + 'vipDisclaimer': + 'تعتمد الفوائد الفعلية على تفاصيل الباقة وقد تتغير مع الإصدارات.', + // Goods names by id + 'ai_001_name': 'الذكاء الاصطناعي الأساسي 100', + 'ai_002_name': 'الذكاء الاصطناعي الأساسي 500', + 'ai_003_name': 'الذكاء الاصطناعي الأساسي 1000', + 'meet_001_name': 'اجتماع أساسي 100', + 'meet_002_name': 'اجتماع أساسي 500', + 'meet_003_name': 'اجتماع أساسي 1000', + 'trans_001_name': 'ترجمة أساسية 100', + 'trans_002_name': 'ترجمة أساسية 500', + 'trans_003_name': 'ترجمة أساسية 1000', + // Goods descriptions by id + 'ai_001_desc': + 'يشمل 100 استخدام. الوصول إلى ميزات الذكاء الاصطناعي الأساسية.', + 'ai_002_desc': + 'يشمل 500 استخدام. الوصول إلى ميزات الذكاء الاصطناعي الأساسية.', + 'ai_003_desc': + 'يشمل 1000 استخدام. الوصول إلى ميزات الذكاء الاصطناعي الأساسية.', + 'meet_001_desc': 'يشمل 100 تسجيل/نسخ لاجتماعات.', + 'meet_002_desc': 'يشمل 500 تسجيل/نسخ لاجتماعات.', + 'meet_003_desc': 'يشمل 1000 تسجيل/نسخ لاجتماعات.', + 'trans_001_desc': 'يشمل 100 ترجمة (صوت/نص).', + 'trans_002_desc': 'يشمل 500 ترجمة (صوت/نص).', + 'trans_003_desc': 'يشمل 1000 ترجمة (صوت/نص).', }; diff --git a/lib/core/translations/language/bg_bg.dart b/lib/core/translations/language/bg_bg.dart index 14f9d770a..7a3d5d277 100644 --- a/lib/core/translations/language/bg_bg.dart +++ b/lib/core/translations/language/bg_bg.dart @@ -136,7 +136,8 @@ const Map bgBG = { "bluetoothInitializationFailure": "Неуспешна инициализация на Bluetooth услугата", "checkingPermissions": "Проверка на разрешенията...", - "lackPermissions": "Липсват Bluetooth разрешения", + "lackPermissions": + "Включете разрешенията за Bluetooth и достъп до локацията, за да се открият и свържат близки устройства", "searchingDevice": "Търсене на устройства...", "findConnectedDevice": "Намерено свързано устройство", "notFound": "Не са намерени свързани Bluetooth устройства", @@ -182,22 +183,9 @@ const Map bgBG = { "conferenceAssistant": "Асистент за бизнес срещи", "conferenceAssistantInfo": "Записване на протоколи в реално време и многоезичен превод", - "studyAssistant": "Асистент за изучаване на езици", - "studyAssistantInfo": "Корекция на произношението и практика на говоренето", - "travelTranslator": "Пътуващ преводач", - "travelTranslatorInfo": - "Гласов превод в реално време и гид за забележителности", - "beSearching": "Търсене:", - "noAssistantFound": "Не е намерен съответстващ AI асистент", + "usageStats": "Статистика за използване", + "usageStatsInfo": "Прегледвайте статистика за използване на приложението", "scene": "Сценарии", - "youHaveSelected": "Избрахте:", - "newFunction": "Нови функции", - "youChoseTry": "Избрахте да опитате", - "aiSinger": "AI певец режим", - "aiSingerInfo": - "Пейте всяка песен с гласа на любимия си изпълнител, дори собствените си творения", - "popularFunction": "Популярни функции", - "tryNow": "Опитай сега", // 商务会议助手页 "startMeeting": "Започни среща", @@ -213,8 +201,12 @@ const Map bgBG = { 'meetingTranscript': 'Протокол от срещата', // 设置页 - "systemSettings": "Системни настройки", - "premiumMember": "Премиум член", + "systemSettings": "Системни настройки", //系统设置 + "vipRecharge": "Преплащане за VIP член", //会员充值 + "aiRecharge": "Премиум член", //AI充值 + "translationRecharge": "Преплащане за превод", //翻译充值 + "meetingAssistantRecharge": "Преплащане за Сообщението", //会议助手充值 + "cloudSync": "Преплащане за синхронизация в облака", //云端同步 "joinDevice": "Свързано устройство:", "accountId": "ID на акаунта:", "systemSetting": "Системни настройки", @@ -286,7 +278,7 @@ const Map bgBG = { //Agent 模块 // agent View - "XiaoYan": "Сяо Ян", + "Lily": "Lily", "clearChatHistory": "Изчисти историята на чата", "online": "Онлайн", "inputMessage": "Въведете съобщение...", @@ -716,7 +708,7 @@ const Map bgBG = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Сдвояване на устройства", - "connectYourDeepSoundHeadset": "Свържете вашите DeepSound слушалки", + "connectYourDeepSoundHeadset": "Свържете вашите Voitrans слушалки", "bind": "Свържи", "authorizeBind": "Разреши свързването", "enterCode": "Моля въведете кода", @@ -779,7 +771,7 @@ const Map bgBG = { 'pleaseGrantNecessaryPermissions': 'Моля предоставете необходимите разрешения', 'checkFailedRetry': 'Неуспешна проверка, моля опитайте отново', - 'pleaseConnectDeepSoundDevice': 'Моля свържете DeepSound устройство', + 'pleaseConnectDeepSoundDevice': 'Моля свържете Voitrans устройство', 'verificationFailedRetry': 'Неуспешна проверка, моля опитайте отново', 'invalidDeviceAddress': 'Невалиден адрес на устройството', 'bleDeviceAssociationFailed': 'Неуспешна асоциация с BLE устройството', diff --git a/lib/core/translations/language/cs_cz.dart b/lib/core/translations/language/cs_cz.dart index b432d7a87..252c757e4 100644 --- a/lib/core/translations/language/cs_cz.dart +++ b/lib/core/translations/language/cs_cz.dart @@ -136,7 +136,8 @@ const Map csCZ = { "initializing": "Inicializace...", "bluetoothInitializationFailure": "Inicializace služby Bluetooth selhala", "checkingPermissions": "Kontrola oprávnění...", - "lackPermissions": "Chybí oprávnění Bluetooth", + "lackPermissions": + "Chybí oprávnění Bluetooth a přístup k lokaci, pro nalezení a připojení zařízení", "searchingDevice": "Hledání zařízení...", "findConnectedDevice": "Nalezeno připojené zařízení", "notFound": "Nenalezena žádná připojená Bluetooth zařízení", @@ -181,21 +182,9 @@ const Map csCZ = { "conferenceAssistant": "Asistent obchodních schůzek", "conferenceAssistantInfo": "Záznam zápisů v reálném čase a vícejazyčný překlad", - "studyAssistant": "Asistent pro výuku jazyků", - "studyAssistantInfo": "Korekce výslovnosti a procvičování mluvení", - "travelTranslator": "Cestovní překladač", - "travelTranslatorInfo": "Hlasový překlad v reálném čase a průvodce památkami", - "beSearching": "Hledání:", - "noAssistantFound": "Nebyl nalezen odpovídající AI asistent", + "usageStats": "Statistika použití", + "usageStatsInfo": "Zobrazit statistiku použití aplikace", "scene": "Scénáře", - "youHaveSelected": "Vybrali jste:", - "newFunction": "Nové funkce", - "youChoseTry": "Rozhodli jste se vyzkoušet", - "aiSinger": "AI zpěvák režim", - "aiSingerInfo": - "Zpívejte jakoukoliv píseň hlasem vašeho oblíbeného umělce, dokonce i vaše vlastní tvorbu", - "popularFunction": "Populární funkce", - "tryNow": "Vyzkoušet nyní", // 商务会议助手页 "startMeeting": "Začít schůzku", @@ -211,8 +200,12 @@ const Map csCZ = { 'meetingTranscript': 'Přepis schůzky', // 设置页 - "systemSettings": "Systémová nastavení", - "premiumMember": "Prémiový člen", + "systemSettings": "Systémová nastavení", //系统设置 + "vipRecharge": "Přepayka VIP členství", //会员充值 + "aiRecharge": "Prémiový člen", //AI充值 + "translationRecharge": "Přepayka překladu", //翻译充值 + "meetingAssistantRecharge": "Přepayka asistenta schůzky", //会议助手充值 + "cloudSync": "Přepayka synchronizace v cloudu", //云端同步 "joinDevice": "Připojené zařízení:", "accountId": "ID účtu:", "systemSetting": "Systémová nastavení", @@ -283,7 +276,7 @@ const Map csCZ = { //Agent 模块 // agent View - "XiaoYan": "Xiao Yan", + "Lily": "Lily", "clearChatHistory": "Vymazat historii chatu", "online": "Online", "inputMessage": "Zadejte zprávu...", @@ -310,7 +303,7 @@ const Map csCZ = { "swipeUpForMore": "Potáhněte nahoru pro více", // Agent controller - 'welcomeMessage': 'Ahoj! Jsem **Xiao Yan**, mohu vám pomoci s:\n\n' + 'welcomeMessage': 'Ahoj! Jsem **Lily**, mohu vám pomoci s:\n\n' '• Odpovídáním na otázky a poskytováním informací\n\n' '• Překladem řeči v reálném čase\n\n' '• Doporučováním hudby a podcastů\n\n' @@ -705,7 +698,7 @@ const Map csCZ = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Párování zařízení", - "connectYourDeepSoundHeadset": "Připojte svá sluchátka DeepSound", + "connectYourDeepSoundHeadset": "Připojte svá sluchátka Voitrans", "bind": "Svázat", "authorizeBind": "Autorizovat svázání", "enterCode": "Zadejte prosím kód", @@ -766,7 +759,7 @@ const Map csCZ = { 'retryAuthorization': 'Zkusit autorizaci znovu', 'pleaseGrantNecessaryPermissions': 'Udělte prosím potřebná oprávnění', 'checkFailedRetry': 'Kontrola selhala, zkuste to prosím znovu', - 'pleaseConnectDeepSoundDevice': 'Připojte prosím zařízení DeepSound', + 'pleaseConnectDeepSoundDevice': 'Připojte prosím zařízení Voitrans', 'verificationFailedRetry': 'Ověření selhalo, zkuste to prosím znovu', 'invalidDeviceAddress': 'Neplatná adresa zařízení', 'bleDeviceAssociationFailed': 'Přidružení BLE zařízení selhalo', diff --git a/lib/core/translations/language/da_dk.dart b/lib/core/translations/language/da_dk.dart index 31bfa72fb..bdd1c77b6 100644 --- a/lib/core/translations/language/da_dk.dart +++ b/lib/core/translations/language/da_dk.dart @@ -136,7 +136,8 @@ const Map daDK = { "bluetoothInitializationFailure": "Bluetooth service initialisering mislykkedes", "checkingPermissions": "Kontrollerer tilladelser...", - "lackPermissions": "Mangler Bluetooth tilladelser", + "lackPermissions": + "Mangler Bluetooth tilladelser og tilladelse til at tilgå lokationen", "searchingDevice": "Søger efter enheder...", "findConnectedDevice": "Fandt tilsluttet enhed", "notFound": "Ingen tilsluttede Bluetooth enheder fundet", @@ -181,22 +182,9 @@ const Map daDK = { "conferenceAssistant": "Forretningsmøde assistent", "conferenceAssistantInfo": "Realtidsoptagelse af mødereferater og flersproget oversættelse", - "studyAssistant": "Sprogindlærings assistent", - "studyAssistantInfo": "Udtalerettelse og taleøvelse", - "travelTranslator": "Rejseoversætter", - "travelTranslatorInfo": - "Realtids stemmeoversættelse og turistattraktionsguide", - "beSearching": "Søger:", - "noAssistantFound": "Ingen tilsvarende AI assistent fundet", + "usageStats": "Brugsstatistik", + "usageStatsInfo": "Se statistik for brug af appen", "scene": "Scenarier", - "youHaveSelected": "Du har valgt:", - "newFunction": "Nye funktioner", - "youChoseTry": "Du valgte at prøve", - "aiSinger": "AI sanger tilstand", - "aiSingerInfo": - "Syng enhver sang med din yndlingskunstners stemme, endda dine egne kreationer", - "popularFunction": "Populære funktioner", - "tryNow": "Prøv nu", // 商务会议助手页 "startMeeting": "Start møde", @@ -212,8 +200,12 @@ const Map daDK = { 'meetingTranscript': 'Mødeudskrift', // 设置页 - "systemSettings": "Systemindstillinger", - "premiumMember": "Premium medlem", + "systemSettings": "Systemindstillinger", //系统设置 + "vipRecharge": "Opladning af VIP medlem", //会员充值 + "aiRecharge": "Premium medlem", //AI充值 + "translationRecharge": "Opladning af oversetning", //翻译充值 + "meetingAssistantRecharge": "Opladning af mødeassistent", //会议助手充值 + "cloudSync": "Opladning af cloud-synkronisering", //云端同步 "joinDevice": "Tilsluttet enhed:", "accountId": "Konto-ID:", "systemSetting": "Systemindstillinger", @@ -285,7 +277,7 @@ const Map daDK = { //Agent 模块 // agent View - "XiaoYan": "Xiao Yan", + "Lily": "Lily", "clearChatHistory": "Ryd chathistorik", "online": "Online", "inputMessage": "Indtast besked...", @@ -312,7 +304,7 @@ const Map daDK = { "swipeUpForMore": "Stryg op for mere", // Agent controller - 'welcomeMessage': 'Hej! Jeg er **Xiao Yan**, jeg kan hjælpe dig med:\n\n' + 'welcomeMessage': 'Hej! Jeg er **Lily**, jeg kan hjælpe dig med:\n\n' '• Besvare spørgsmål og give information\n\n' '• Realtids stemmeoversættelse\n\n' '• Anbefale musik og podcasts\n\n' @@ -706,7 +698,7 @@ const Map daDK = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Enhedsparring", - "connectYourDeepSoundHeadset": "Tilslut dine DeepSound øretelefoner", + "connectYourDeepSoundHeadset": "Tilslut dine Voitrans øretelefoner", "bind": "Bind", "authorizeBind": "Autoriser binding", "enterCode": "Indtast venligst kode", @@ -767,7 +759,7 @@ const Map daDK = { 'retryAuthorization': 'Prøv autorisation igen', 'pleaseGrantNecessaryPermissions': 'Giv venligst de nødvendige tilladelser', 'checkFailedRetry': 'Kontrol mislykkedes, prøv venligst igen', - 'pleaseConnectDeepSoundDevice': 'Tilslut venligst DeepSound enhed', + 'pleaseConnectDeepSoundDevice': 'Tilslut venligst Voitrans enhed', 'verificationFailedRetry': 'Verificering mislykkedes, prøv venligst igen', 'invalidDeviceAddress': 'Ugyldig enhedsadresse', 'bleDeviceAssociationFailed': 'BLE enhedsassociation mislykkedes', diff --git a/lib/core/translations/language/de_de.dart b/lib/core/translations/language/de_de.dart index f5bdbc994..ccc28727f 100644 --- a/lib/core/translations/language/de_de.dart +++ b/lib/core/translations/language/de_de.dart @@ -139,7 +139,8 @@ const Map deDE = { "bluetoothInitializationFailure": "Bluetooth-Dienst-Initialisierung fehlgeschlagen", "checkingPermissions": "Berechtigungen werden überprüft...", - "lackPermissions": "Bluetooth-Berechtigungen erforderlich", + "lackPermissions": + "Bluetooth-Berechtigungen erforderlich und Zugriff auf die Lage erforderlich", "searchingDevice": "Geräte werden gesucht...", "findConnectedDevice": "Verbundenes Gerät gefunden", "notFound": "Keine verbundenen Bluetooth-Geräte gefunden", @@ -185,22 +186,10 @@ const Map deDE = { "conferenceAssistant": "Geschäftstreffen-Assistent", "conferenceAssistantInfo": "Echtzeit-Protokollaufzeichnung und mehrsprachige Übersetzung", - "studyAssistant": "Sprachlern-Assistent", - "studyAssistantInfo": "Aussprachekorrektur und Sprechpraxis", - "travelTranslator": "Reise-Übersetzer", - "travelTranslatorInfo": - "Echtzeit-Sprachübersetzung und Sehenswürdigkeiten-Führung", - "beSearching": "Suche nach:", - "noAssistantFound": "Kein entsprechender KI-Assistent gefunden", + "usageStats": "Benutzungsstatistik", + "usageStatsInfo": + "Sehen Sie sich die Statistiken für die Benutzung der App an", "scene": "Szenarien", - "youHaveSelected": "Sie haben ausgewählt:", - "newFunction": "Neue Funktionen", - "youChoseTry": "Sie haben sich entschieden zu versuchen", - "aiSinger": "KI-Sänger-Modus", - "aiSingerInfo": - "Singen Sie jedes Lied in der Stimme Ihres Lieblingskünstlers, sogar Ihre eigenen Kreationen", - "popularFunction": "Beliebte Funktionen", - "tryNow": "Jetzt versuchen", // 商务会议助手页 "startMeeting": "Meeting starten", @@ -216,8 +205,12 @@ const Map deDE = { 'meetingTranscript': 'Meeting-Transkript', // 设置页 - "systemSettings": "Systemeinstellungen", - "premiumMember": "Premium-Mitgliedschaft", + "systemSettings": "Systemeinstellungen", //系统设置 + "vipRecharge": "VIP-Mitgliedschaft-Recharge", //会员充值 + "aiRecharge": "Premium-Mitgliedschaft", //AI充值 + "translationRecharge": "Übersetzungs-Recharge", //翻译充值 + "meetingAssistantRecharge": "Meeting-Assistent-Recharge", //会议助手充值 + "cloudSync": "Cloud-Synchronisierung-Recharge", //云端同步 "joinDevice": "Verbundenes Gerät:", "accountId": "Konto-ID:", "systemSetting": "Systemeinstellungen", @@ -288,7 +281,7 @@ const Map deDE = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Chat-Verlauf löschen", "online": "Online", "inputMessage": "Nachricht eingeben...", @@ -315,7 +308,7 @@ const Map deDE = { "swipeUpForMore": "Nach oben wischen für mehr", // Agent controller - 'welcomeMessage': 'Hallo! Ich bin **XiaoYan**, ich kann Ihnen helfen:\n\n' + 'welcomeMessage': 'Hallo! Ich bin **Lily**, ich kann Ihnen helfen:\n\n' '• Fragen beantworten und Informationen bereitstellen\n\n' '• Echtzeit-Sprachübersetzung\n\n' '• Musik und Podcasts empfehlen\n\n' @@ -719,7 +712,7 @@ const Map deDE = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Gerätekopplung", - "connectYourDeepSoundHeadset": "Verbinden Sie Ihr DeepSound-Headset", + "connectYourDeepSoundHeadset": "Verbinden Sie Ihr Voitrans-Headset", "bind": "Binden", "authorizeBind": "Bindung autorisieren", "enterCode": "Bitte geben Sie den Code ein", @@ -737,7 +730,7 @@ const Map deDE = { "readyToPair": "Bereit zum Koppeln", "preparingPairingEnvironment": "Kopplungsumgebung wird vorbereitet...", "connectDeepSoundViaSystemBluetooth": - "Bitte verbinden Sie zuerst das DeepSound-Gerät über System-Bluetooth", + "Bitte verbinden Sie zuerst das Voitrans-Gerät über System-Bluetooth", "deviceBindingConfirm": "Gerätebindungsbestätigung...", "establishingDeviceAssociation": "Gerätezuordnung wird hergestellt...", "waitingCompletePairing": "Bitte warten, Kopplung wird abgeschlossen...", @@ -746,7 +739,7 @@ const Map deDE = { // 配对错误状态文本 "ensureConnectedDeepSoundDevice": - "Bitte stellen Sie sicher, dass das verbundene Gerät DeepSound ist", + "Bitte stellen Sie sicher, dass das verbundene Gerät Voitrans ist", "deviceAlreadyBoundToOtherAccount": "Dieses Gerät ist bereits an ein anderes Konto gebunden", "deviceAssociationFailed": @@ -783,7 +776,7 @@ const Map deDE = { 'Bitte erteilen Sie die erforderlichen Berechtigungen', 'checkFailedRetry': 'Überprüfung fehlgeschlagen, bitte versuchen Sie es erneut', - 'pleaseConnectDeepSoundDevice': 'Bitte verbinden Sie das DeepSound-Gerät', + 'pleaseConnectDeepSoundDevice': 'Bitte verbinden Sie das Voitrans-Gerät', 'verificationFailedRetry': 'Verifizierung fehlgeschlagen, bitte versuchen Sie es erneut', 'invalidDeviceAddress': 'Ungültige Geräteadresse', @@ -880,4 +873,97 @@ const Map deDE = { "乌尔都语": "Urdu", "波斯语": "Persisch", "斯瓦希里语": "Swahili", + // Guide overlay keys (placeholders in English) + "sayLilyHint": "Say \"Lily Lily\" to the mic", + "imHere": "I’m here~", + "guideWelcomeWakeAI": "Welcome to Voitrans! First, wake your AI “Lily”.", + "aiReplyCompleted": "Lily has finished the reply!", + "aiThinking": "Lily is thinking…", + "guideAwakenSuccess": "Great! Lily is awake—ask it to help.", + "confirmInput": "Confirm Input", + "guideClickSendPrefix": "Tap send, ", + "guideWeatherStart": "Tell Lily: “What’s the weather today?”", + "guideWeatherInput": "What’s the weather today?", + "guideWeatherSend": "Lily will report the weather in real time", + "guideWeatherEnd": "Lily can report weather live—try other commands", + "guideMusicStart": "Tell Lily: “Play some rock music”", + "guideMusicInput": "Play some rock music", + "guideMusicSend": "Lily will find music for you", + "guideMusicEnd": "Lily found music—try other commands", + "guideNavStart": "Tell Lily: “Navigate to Shenzhen Airport”", + "guideNavInput": "Navigate to Shenzhen Airport", + "guideNavSend": "Lily will plan the route", + "guideNavEnd": "Navigation set—try other commands", + "guideMeetingStart": + "Tell Lily: “Create a meeting reminder for 3pm tomorrow”", + "guideMeetingInput": "Create a meeting reminder for 3pm tomorrow", + "guideMeetingSend": "Lily will create a meeting reminder", + "guideMeetingEnd": "Reminder created—try other commands", + "guidePhoneStart": "Tell Lily: “Call Mom”", + "guidePhoneInput": "Call Mom", + "guidePhoneSend": "Lily will make the call", + "guidePhoneEnd": "Call placed—try other commands", + "guideSmsStart": "Tell Lily: “Send Dad a text: I’ll be home for dinner”", + "guideSmsInput": "Text Dad: I’ll be home for dinner", + "guideSmsSend": "Lily will send the message", + "guideSmsEnd": "Message sent—voice control is convenient", + // Goods pages common footer (placeholders) + 'appNameProvider': 'App: VoiTrans · Provided by: VoiTrans', + 'copyrightVoitrans': '© 2025 Voitrans Technologies', + + // Meeting goods page (placeholders) + 'meetingHeaderDesc1': + 'Audio recording, transcription, keyword search, and auto summary.', + 'meetingHeaderDesc2': + 'Suitable for business meetings, class notes, and interview minutes.', + 'meetingAutoRecording': 'Auto recording and segmentation', + 'meetingSpeakerTagging': 'Speaker labeling', + 'meetingLongAudioTranscription': 'Long audio transcription', + 'meetingKeyPointSummary': 'Key point extraction', + 'meetingTopicNameSearch': 'Search by topic/name', + 'meetingTimelineJump': 'Timeline jump', + 'meetingExportSummary': 'Export minutes', + 'meetingMultiPlatformShare': 'Share across platforms', + 'meetingDisclaimer': + 'Use in quiet environments; performance depends on microphone and network.', + + // Translation goods page (placeholders) + 'transHeaderDesc1': + 'Echtzeit-Simultandolmetschen und Textübersetzung mit Sprachwiedergabe bei niedriger Latenz.', + 'transHeaderDesc2': 'Geeignet für Reisen, Geschäft, Reden und Lehre.', + 'translateZhEn': 'Chinesisch-Englisch-Übersetzung', + 'lowLatencyVoiceBroadcast': 'Sprachübertragung mit niedriger Latenz', + 'multiLangTextTranslation': 'Mehrsprachige Textübersetzung', + 'termOptimizationContextPreserve': + 'Terminologieoptimierung und Kontextwahrung', + 'lexiconTourBusinessLife': 'Lexika für Reise/Geschäft/Alltag', + 'personalVocabularyMemory': 'Persönlicher Wortschatzspeicher', + 'conversationSubtitleLargeText': 'Konversationsuntertitel / Großtext-Modus', + 'colorHighContrast': 'Hoher Farbkontrast', + 'transDisclaimer': + 'Die Übersetzungsleistung hängt von Mikrofon, Netzwerklatenz und Umgebungsgeräuschen ab.', + + 'trans_001_name': 'Übersetzung Basis 100', + 'trans_002_name': 'Übersetzung Basis 500', + 'trans_003_name': 'Übersetzung Basis 1000', + + 'trans_001_desc': 'Enthält 100 Übersetzungen (Stimme/Text).', + 'trans_002_desc': 'Enthält 500 Übersetzungen (Stimme/Text).', + 'trans_003_desc': 'Enthält 1000 Übersetzungen (Stimme/Text).', + + // VIP goods page (placeholders) + 'benefitsOverview': 'Übersicht der Vorteile', + 'higherQuota': 'Höheres Nutzungskontingent', + 'lowerUnitPrice': 'Niedrigerer Stückpreis', + 'priorityService': 'Priorisierter Service', + 'exclusiveSupportChannel': 'Dedizierter Supportkanal', + 'fastResponse': 'Schnelle Problemreaktion', + 'eventsAndDiscounts': 'Veranstaltungen und Rabatte', + 'memberExclusiveEvents': 'Exklusive Events für Mitglieder', + 'longTermDiscountPlan': 'Langfristiger Rabattplan', + 'generalPrivileges': 'Allgemeine Privilegien', + 'earlyAccessNewFeatures': 'Frühzeitiger Zugriff auf neue Funktionen', + 'crossDeviceSync': 'Geräteübergreifende Synchronisierung', + 'vipDisclaimer': + 'Die tatsächlichen Vorteile hängen von den Paketdetails ab und können sich mit Versionen ändern.', }; diff --git a/lib/core/translations/language/el_gr.dart b/lib/core/translations/language/el_gr.dart index fec4d66c4..a39a65cd4 100644 --- a/lib/core/translations/language/el_gr.dart +++ b/lib/core/translations/language/el_gr.dart @@ -138,7 +138,8 @@ const Map elGR = { "bluetoothInitializationFailure": "Αποτυχία αρχικοποίησης υπηρεσίας Bluetooth", "checkingPermissions": "Έλεγχος δικαιωμάτων...", - "lackPermissions": "Απαιτούνται δικαιώματα Bluetooth", + "lackPermissions": + "Απαιτούνται δικαιώματα Bluetooth και πρόσβαση στην τοποθεσία", "searchingDevice": "Αναζήτηση συσκευών...", "findConnectedDevice": "Βρέθηκε συνδεδεμένη συσκευή", "notFound": "Δεν βρέθηκαν συνδεδεμένες συσκευές Bluetooth", @@ -183,22 +184,9 @@ const Map elGR = { "conferenceAssistant": "Βοηθός επιχειρηματικών συναντήσεων", "conferenceAssistantInfo": "Καταγραφή πρακτικών σε πραγματικό χρόνο και πολυγλωσσική μετάφραση", - "studyAssistant": "Βοηθός εκμάθησης γλωσσών", - "studyAssistantInfo": "Διόρθωση προφοράς και εξάσκηση ομιλίας", - "travelTranslator": "Μεταφραστής ταξιδιού", - "travelTranslatorInfo": - "Φωνητική μετάφραση σε πραγματικό χρόνο και οδηγός αξιοθέατων", - "beSearching": "Αναζήτηση:", - "noAssistantFound": "Δεν βρέθηκε αντίστοιχος βοηθός AI", + "usageStats": "Στατιστικά χρήσης", + "usageStatsInfo": "Δείτε τα στατιστικά χρήσης της εφαρμογής", "scene": "Σενάρια", - "youHaveSelected": "Έχετε επιλέξει:", - "newFunction": "Νέες λειτουργίες", - "youChoseTry": "Επιλέξατε να δοκιμάσετε", - "aiSinger": "Λειτουργία τραγουδιστή AI", - "aiSingerInfo": - "Τραγουδήστε οποιοδήποτε τραγούδι με τη φωνή του αγαπημένου σας καλλιτέχνη, ακόμα και τις δικές σας δημιουργίες", - "popularFunction": "Δημοφιλείς λειτουργίες", - "tryNow": "Δοκιμάστε τώρα", // 商务会议助手页 "startMeeting": "Έναρξη συνάντησης", @@ -214,8 +202,12 @@ const Map elGR = { 'meetingTranscript': 'Απομαγνητοφώνηση συνάντησης', // 设置页 - "systemSettings": "Ρυθμίσεις συστήματος", - "premiumMember": "Premium μέλος", + "systemSettings": "Ρυθμίσεις συστήματος", //系统设置 + "vipRecharge": "Περιορέωση VIP μέλους", //会员充值 + "aiRecharge": "Premium μέλος", //AI充值 + "translationRecharge": "Περιορέωση μετάφρασης", //翻译充值 + "meetingAssistantRecharge": "Περιορέωση βοηθού συνάντησης", //会议助手充值 + "cloudSync": "Περιορέωση συγχρονισμού στο cloud", //云端同步 "joinDevice": "Συνδεδεμένη συσκευή:", "accountId": "ID λογαριασμού:", "systemSetting": "Ρυθμίσεις συστήματος", @@ -287,7 +279,7 @@ const Map elGR = { //Agent 模块 // agent View - "XiaoYan": "Xiao Yan", + "Lily": "Lily", "clearChatHistory": "Εκκαθάριση ιστορικού συνομιλίας", "online": "Σε σύνδεση", "inputMessage": "Εισάγετε μήνυμα...", @@ -314,14 +306,13 @@ const Map elGR = { "swipeUpForMore": "Σύρετε προς τα πάνω για περισσότερα", // Agent controller - 'welcomeMessage': - 'Γεια σας! Είμαι ο **Xiao Yan**, μπορώ να σας βοηθήσω με:\n\n' - '• Απάντηση σε ερωτήσεις και παροχή πληροφοριών\n\n' - '• Μετάφραση φωνής σε πραγματικό χρόνο\n\n' - '• Προτάσεις μουσικής και podcasts\n\n' - '• Φωνητικές σημειώσεις και υπενθυμίσεις\n\n' - '• Έλεγχος λειτουργιών ακουστικών\n\n' - 'Με τι μπορώ να σας βοηθήσω;', + 'welcomeMessage': 'Γεια σας! Είμαι ο **Lily**, μπορώ να σας βοηθήσω με:\n\n' + '• Απάντηση σε ερωτήσεις και παροχή πληροφοριών\n\n' + '• Μετάφραση φωνής σε πραγματικό χρόνο\n\n' + '• Προτάσεις μουσικής και podcasts\n\n' + '• Φωνητικές σημειώσεις και υπενθυμίσεις\n\n' + '• Έλεγχος λειτουργιών ακουστικών\n\n' + 'Με τι μπορώ να σας βοηθήσω;', 'microphonePermission': 'Μικρόφωνο', 'voiceRecognitionPermissionText': 'Η λειτουργία αναγνώρισης φωνής χρειάζεται πρόσβαση στο μικρόφωνο.', @@ -717,7 +708,7 @@ const Map elGR = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Σύζευξη συσκευής", - "connectYourDeepSoundHeadset": "Συνδέστε τα ακουστικά DeepSound", + "connectYourDeepSoundHeadset": "Συνδέστε τα ακουστικά Voitrans", "bind": "Σύνδεση", "authorizeBind": "Εξουσιοδότηση σύνδεσης", "enterCode": "Εισάγετε κωδικό", @@ -777,7 +768,7 @@ const Map elGR = { 'retryAuthorization': 'Επανάληψη εξουσιοδότησης', 'pleaseGrantNecessaryPermissions': 'Παραχωρήστε τα απαραίτητα δικαιώματα', 'checkFailedRetry': 'Αποτυχία ελέγχου, δοκιμάστε ξανά', - 'pleaseConnectDeepSoundDevice': 'Συνδέστε συσκευή DeepSound', + 'pleaseConnectDeepSoundDevice': 'Συνδέστε συσκευή Voitrans', 'verificationFailedRetry': 'Αποτυχία επαλήθευσης, δοκιμάστε ξανά', 'invalidDeviceAddress': 'Μη έγκυρη διεύθυνση συσκευής', 'bleDeviceAssociationFailed': 'Αποτυχία συσχέτισης συσκευής BLE', diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index d01651e54..bfac53331 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -136,7 +136,8 @@ const Map enUS = { "initializing": "Initializing...", "bluetoothInitializationFailure": "Bluetooth service initialization failed", "checkingPermissions": "Checking permissions...", - "lackPermissions": "Bluetooth permissions required", + "lackPermissions": + "Enable Bluetooth and location permissions to discover and connect to nearby devices", "searchingDevice": "Searching for devices...", "findConnectedDevice": "Found connected device", "notFound": "No connected Bluetooth devices found", @@ -165,7 +166,7 @@ const Map enUS = { "exploreSlogan": "Smart Earbuds, Infinite Possibilities", "exploreSlogan1": "Intelligent companion, Infinite Possibilities", "exploreSlogan2": "Smart glasses, Infinite Possibilities", - "exploreAi": "Explore AI features provided by DeepSound", + "exploreAi": "Explore AI features provided by Voitrans", "aiFunction": "AI Features", "allRoundAssistant": "All-round Assistant", "allRoundAssistantInfo": "Helps solve various problems", @@ -181,21 +182,213 @@ const Map enUS = { "conferenceAssistant": "Business Meeting Assistant", "conferenceAssistantInfo": "Real-time minutes recording and multilingual translation", - "studyAssistant": "Language Learning Assistant", - "studyAssistantInfo": "Pronunciation correction and speaking practice", - "travelTranslator": "Travel Translator", - "travelTranslatorInfo": "Real-time voice translation and attraction guides", - "beSearching": "Searching for:", - "noAssistantFound": "No corresponding AI assistant found", + "usageStats": "Usage Statistics", + "usageStatsInfo": "View Your Usage Statistics", "scene": "Scenarios", - "youHaveSelected": "You've selected:", - "newFunction": "New Features", - "youChoseTry": "You've chosen to try", - "aiSinger": "AI Singer Mode", - "aiSingerInfo": - "Sing any song in your favorite artist's voice, even your own creations", - "popularFunction": "Popular Features", - "tryNow": "Try Now", + + /// 用量统计模块 + "aiChat": "AI Chat", + "translationStats": "Translation Statistics", + "meetingStats": "Meeting Statistics", + "today": "Today", + "thisWeek": "This Week", + "thisMonth": "This Month", + "history": "History", + "currentSessionStats": "Current Session Stats", + "translationCount": "Translation Count", + "sessionDuration": "Session Duration", + "characters": "Characters", + "recordingDuration": "Recording Duration", + "totalApiCalls": "Total API Calls", + "sessionCount": "Session Count", + "totalCharacters": "Total Characters", + "apiCallStats": "API Call Stats", + "translationApi": "Translation API", + "asr": "ASR", + "tts": "TTS", + // "aiSummary": "AI Summary", + "dataVolumeStats": "Data Volume Stats", + "sourceTextCharacters": "Source Text Characters", + "ttsTextCharacters": "TTS Text Characters", + "effectiveAudioDuration": "Effective Audio Duration", + "totalSessionDuration": "Total Session Duration", + "costStats": "Cost Stats", + "estimatedCost": "Estimated Cost", + "dailyCostThreshold": "Daily Cost Threshold", + "monthlyCostThreshold": "Monthly Cost Threshold", + "usageBehaviorStats": "Usage Behavior Stats", + "mostUsedLanguagePair": "Most-used language pair", + "mostUsedMode": "Most-used mode", + "asrApiCalls": "ASR Calls", + "totalAudioDuration": "Total Audio Duration", + "audioFileSize": "Audio File Size", + "transcriptBytes": "Transcript Bytes", + //通知文案 + "loadFailed": "Load Failed", + "statsLoadFailedTryLater": + "Statistics failed to load. Please try again later.", + "cleanupComplete": "Cleanup Complete", + "cleanedOldData": "Cleaned data older than 90 days", + "exportFeature": "Export Feature", + "exportInDevelopment": "Data export is under development", + + // 商品页面(AI/翻译/会议) + 'availablePackages': 'Available Packages', + 'remainingQuota': 'Remaining Quota', + 'totalQuota': 'Total Quota', + 'remaining': 'Remaining', + 'featureDescription': 'Feature Description', + // AI 功能卡片 + 'musicAndRecommendation': 'Music & Recommendation', + 'navigationAndRoutes': 'Navigation & Routes', + 'nearbySearch': 'Nearby Search', + 'newsInformation': 'News', + 'communicationAssistant': 'Communication Assistant', + // 翻译功能卡片 + 'realtimeTranscription': 'Real-time Transcription', + // 'textTranslation' exists + 'sceneLexicon': 'Scenario Glossary', + 'subtitleDisplay': 'Subtitles', + // 会议功能卡片 + 'meetingRecording': 'Meeting Recording', + 'transcriptionAndSummary': 'Transcription & Summary', + 'keywordSearch': 'Keyword Search', + 'collaborationSharing': 'Collaboration & Sharing', + // 商品名称(按ID) + 'ai_001_name': 'AI Basic 100', + 'ai_002_name': 'AI Basic 500', + 'ai_003_name': 'AI Basic 1000', + 'meet_001_name': 'Meeting Basic 100', + 'meet_002_name': 'Meeting Basic 500', + 'meet_003_name': 'Meeting Basic 1000', + 'trans_001_name': 'Translation Basic 100', + 'trans_002_name': 'Translation Basic 500', + 'trans_003_name': 'Translation Basic 1000', + + // 商品描述(按ID) + 'ai_001_desc': 'Includes 100 uses. Access core AI features.', + 'ai_002_desc': 'Includes 500 uses. Access core AI features.', + 'ai_003_desc': 'Includes 1000 uses. Access core AI features.', + 'meet_001_desc': 'Includes 100 meeting recordings/transcriptions.', + 'meet_002_desc': 'Includes 500 meeting recordings/transcriptions.', + 'meet_003_desc': 'Includes 1000 meeting recordings/transcriptions.', + 'trans_001_desc': 'Includes 100 translations (voice/text).', + 'trans_002_desc': 'Includes 500 translations (voice/text).', + 'trans_003_desc': 'Includes 1000 translations (voice/text).', + + // 商品页通用页脚 + 'appNameProvider': 'App: VoiTrans · Provided by: VoiTrans', + 'iapCustomerServiceQQLabel': 'IAP customer service QQ: @qq', + 'copyrightVoitrans': '© 2025 Voitrans Technologies', + + // 会议商品页描述 + 'meetingHeaderDesc1': + 'Audio recording, transcription, keyword search, and auto summary to boost meeting efficiency.', + 'meetingHeaderDesc2': + 'Suitable for business meetings, class notes, and interview minutes.', + 'meetingAutoRecording': 'Auto recording and segmentation', + 'meetingSpeakerTagging': 'Speaker labeling', + 'meetingLongAudioTranscription': 'Long audio transcription', + 'meetingKeyPointSummary': 'Key point extraction', + 'meetingTopicNameSearch': 'Search by topic/name', + 'meetingTimelineJump': 'Timeline jump', + 'meetingExportSummary': 'Export minutes', + 'meetingMultiPlatformShare': 'Share across platforms', + 'meetingDisclaimer': + 'Use in quiet environments; performance depends on microphone and network.', + + // 同传/翻译商品页描述 + 'transHeaderDesc1': + 'Real-time simultaneous interpretation and text translation with low-latency voice.', + 'transHeaderDesc2': 'Suitable for travel, business, speeches, and teaching.', + 'translateZhEn': 'Chinese-English translation', + 'lowLatencyVoiceBroadcast': 'Low-latency voice broadcast', + 'multiLangTextTranslation': 'Multilingual text translation', + 'termOptimizationContextPreserve': + 'Terminology optimization and context preservation', + 'lexiconTourBusinessLife': 'Travel/Business/Life lexicons', + 'personalVocabularyMemory': 'Personal vocabulary memory', + 'conversationSubtitleLargeText': 'Conversation subtitles / Large-text mode', + 'colorHighContrast': 'High color contrast', + 'transDisclaimer': + 'Translation performance depends on microphone, network latency, and ambient noise.', + + // AI 商品页描述 + 'aiHeaderDesc1': + 'Longer conversation quota, more tool calls, extended context length.', + 'aiHeaderDesc2': + 'Suitable for agent Q&A, summarization, analysis, and task execution.', + 'musicSearchPlaybackControl': 'Music library search and playback control', + 'smartPlaylistAndSimilarRecommendations': + 'Smart playlist and similar song recommendations', + 'routePlanningAndCongestionAvoidance': + 'Route planning and congestion avoidance', + 'voiceBroadcastAndWaypoints': 'Voice broadcast and waypoint settings', + 'realTimeWeatherForecast': 'Real-time weather and forecast', + 'clothingAdviceAndTravelReminders': 'Clothing advice and travel reminders', + 'poiSearchFoodGasParking': 'POI search: dining / gas / parking', + 'ratingsHoursAndDirectRoutes': 'Ratings, business hours, and direct routing', + 'hotNewsAndTopicOverview': 'Hot news and topic overview', + 'longArticleSummariesAndKeyPoints': 'Long-article summaries and key points', + 'helpCallAndSMS': 'Help with calls and SMS', + 'templatedMessagesAndScheduleReminders': + 'Template messages and schedule reminders', + 'aiDisclaimer': + 'Features depend on device permissions, service availability, and network conditions.', + + // 会员商品页 + 'benefitsOverview': 'Benefits overview', + 'higherQuota': 'Higher usage quota', + 'lowerUnitPrice': 'Lower unit price', + 'priorityService': 'Priority service', + 'exclusiveSupportChannel': 'Dedicated support channel', + 'fastResponse': 'Fast issue response', + 'eventsAndDiscounts': 'Events and discounts', + 'memberExclusiveEvents': 'Member-exclusive events', + 'longTermDiscountPlan': 'Long-term discount plan', + 'generalPrivileges': 'General privileges', + 'earlyAccessNewFeatures': 'Early access to new features', + 'crossDeviceSync': 'Cross-device sync', + 'vipDisclaimer': + 'Actual benefits depend on the package details and may change with versions.', + + // 新手引导 / Onboarding + 'skip': 'Skip', + 'next': 'Next', + 'sayLilyHint': 'Say "Lily Lily" to the mic', + 'imHere': 'I’m here~', + 'guideWelcomeWakeAI': 'Welcome to Voitrans! First, wake your AI “Lily”.', + 'aiReplyCompleted': 'Lily has finished the reply!', + 'aiThinking': 'Lily is thinking…', + 'guideAwakenSuccess': 'Great! Lily is awake—ask it to help.', + 'confirmInput': 'Confirm Input', + 'guideClickSendPrefix': 'Tap send, ', + // AI 引导步骤 + 'guideWeatherStart': 'Tell Lily: “What’s the weather today?”', + 'guideWeatherInput': 'What’s the weather today?', + 'guideWeatherSend': 'Lily will report the weather in real time', + 'guideWeatherEnd': 'Lily can report weather live—try other commands', + 'guideMusicStart': 'Tell Lily: “Play some rock music”', + 'guideMusicInput': 'Play some rock music', + 'guideMusicSend': 'Lily will find music for you', + 'guideMusicEnd': 'Lily found music—try other commands', + 'guideNavStart': 'Tell Lily: “Navigate to Shenzhen Airport”', + 'guideNavInput': 'Navigate to Shenzhen Airport', + 'guideNavSend': 'Lily will plan the route', + 'guideNavEnd': 'Navigation set—try other commands', + 'guideMeetingStart': + 'Tell Lily: “Create a meeting reminder for 3pm tomorrow”', + 'guideMeetingInput': 'Create a meeting reminder for 3pm tomorrow', + 'guideMeetingSend': 'Lily will create a meeting reminder', + 'guideMeetingEnd': 'Reminder created—try other commands', + 'guidePhoneStart': 'Tell Lily: “Call Mom”', + 'guidePhoneInput': 'Call Mom', + 'guidePhoneSend': 'Lily will make the call', + 'guidePhoneEnd': 'Call placed—try other commands', + 'guideSmsStart': 'Tell Lily: “Send Dad a text: I’ll be home for dinner”', + 'guideSmsInput': 'Text Dad: I’ll be home for dinner', + 'guideSmsSend': 'Lily will send the message', + 'guideSmsEnd': 'Message sent—voice control is convenient', // 商务会议助手页 "startMeeting": "Start Meeting", @@ -438,8 +631,16 @@ const Map enUS = { 'inputAttendees': 'Enter Attendees', // 设置页 - "systemSettings": "System Settings", - "premiumMember": "Premium Membership", + "systemSettings": "System Settings", //系统设置 + "vipRecharge": "VIP Recharge", //会员充值 + "aiRecharge": "AI Recharge", //AI充值 + "translationRecharge": "Translation Recharge", //翻译充值 + "meetingAssistantRecharge": "Meeting Recharge", //会议充值 + "cloudSync": "Cloud Sync Recharge", //云端同步 + "memberService": "Member Service", + "bindRewardTitle": "Rewards", + "bindRewardSimpleDesc": + "First bind device reward has been issued. Tap OK to close.", "joinDevice": "Connected device:", "accountId": "Account ID:", "systemSetting": "System Settings", @@ -517,7 +718,7 @@ const Map enUS = { // Agent模块 //Agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Clear Chat History", "online": "Online", "inputMessage": "Type a message...", @@ -546,9 +747,9 @@ const Map enUS = { "voice_interrupt": "Interrupt", // 语音打断 // 预设提示词 - "presetMusic": "Play some music", // Play some music - "presetNavigation": "Navigation to", // Navigation - "presetWeather": "Today's weather", // Weather + "presetMusic": "Play some music", + "presetNavigation": "Navigation to", + "presetWeather": "Today's weather", "presetNews": "Today's news", // News "presetFood": "Nearby food", // Food "playSomeMusic": "Play some popular music", // 播放一些流行音乐 @@ -558,7 +759,7 @@ const Map enUS = { "nearbyFood": "What delicious food is nearby?", // 美食 // Agent controller - 'welcomeMessage': 'Hello! I\'m **Xiaoyan**, I can help you:\n\n' + 'welcomeMessage': 'Hello! I\'m **Lily**, I can help you:\n\n' '• Answer questions and provide information\n\n' '• Real-time voice translation\n\n' '• Recommend music and podcasts\n\n' @@ -603,7 +804,7 @@ const Map enUS = { //Message_bubble 'generatedByAi': 'Generated by AI', - //music_card + //音乐卡片 music_card 'recommendedMusic': 'Here are some recommended music:', 'playList': 'Playlist', 'moreMusic': 'More music', @@ -618,10 +819,10 @@ const Map enUS = { 'currentLocation': 'Current Location', // 当前位置 'approximate': 'About ', // 大约 'enterRoad': ' enter ', // 进入道路 - "driving": "Driving", - "walking": "Walking", - "bicycling": "Cycling", - "tsransit": "Transit", + "driving": "Driving", // 驾驶 + "walking": "Walking", // 步行 + "bicycling": "Cycling", // 骑行 + "tsransit": "Transit", // 公交 //天气卡片 weather_card 'weatherQuery': 'Weather Query:', @@ -653,6 +854,17 @@ const Map enUS = { "friday": "Fri", "saturday": "Sat", "sunday": "Sun", + // First bind rewards dialog + "firstBindRewardTitle": "Welcome Reward", + "firstBindRewardDesc": "You've received binding rewards.", + "vipLevelLabel": "VIP Level:", + "expiryLabel": "Expiration:", + "notActivated": "Not activated", + "aiChatCreditsLabel": "AI Chat Credits:", + "translationCreditsLabel": "Translation Credits:", + "meetingCreditsLabel": "Meeting Credits:", + "ok": "OK", + // 天气描述 "sunny": "Sunny", "partlyCloudy": "Partly Cloudy", @@ -982,8 +1194,9 @@ const Map enUS = { "pictureTranslation": "Picture Translation", "textTranslation": "Text Translation", - // 翻译主页 + // 翻译主页 (translation_view.dart) "translationHistory": "Translation History", + "historyRecords": "History Records", "enableRecording": "Enable Recording", "viewAudioFiles": "View Audio Files", "headsetUser": "Headset User", @@ -992,13 +1205,21 @@ const Map enUS = { "selectTargetLanguage": "Select Target Language", "me": "Me", //我 "counterPart": "Counterpart", //对方 + "bluetoothCallStateError": + "Please make sure the Bluetooth headset is connected to the phone and in a call state", //请确保蓝牙耳机已连接到手机并处于通话状态 // 通用对话按钮与选项(用于统一语言选择弹窗等) // 翻译键代表的中文文本内容:取消/确认/自动检测 "autoDetect": "Auto Detect", + // Text translation & language detection + "languageDetectionFailed": "Language detection failed", + "translationFailed": "Translation failed", + "pleaseInputText": "Please input text", + "textTooLong": "Text too long, max 5000 characters", // 翻译历史页 "noTranslationHistory": "No translation history", + "clearResults": "Clear Results", "clearHistory": "Clear History", "clearHistoryConfirm": "Are you sure you want to clear all translation history? This action cannot be undone.", @@ -1007,7 +1228,58 @@ const Map enUS = { "deleteSessionConfirm": "Are you sure you want to delete all translation records for this session? This action cannot be undone.", - // 翻译音频文件列表页 + // Picture Translation module + "selectImageSource": "Select Image Source", + "imagePreview": "Image Preview", + "translationResult": "Translation Result", + "recognitionResult": "Recognition Result", + "translatedImagePreview": "Translated Image Preview", + "translatedImageLoadFailed": "Failed to load translated image", + "pleaseSelectImage": "Please select an image", + "imageLoadFailed": "Image load failed", + "imageQuality": "Image Quality", + "maxSize": "Max Size", + "close": "Close", + "setImageQuality": "Set Image Quality", + "currentQuality": "Current quality: @value%", + "setMaxSize": "Set Max Size", + "currentMaxSize": "Current: @pxpx", + "waitingForAction": "Waiting for action", + "pleaseSelectImageToStart": "Please select an image to start", + "chooseImage": "Choose Image", + "openingCamera": "Opening camera...", + "openingPhotoAlbum": "Opening photo album...", + "cancelled": "Cancelled", + "userCancelledSelection": "User cancelled selection", + "translatingImage": "Translating Image", + "callingImageTranslateService": "Calling image translation service...", + "savingRecord": "Saving Record", + "savingTranslationResult": "Saving translation result...", + "translationCompleted": "Translation Completed", + "allStepsCompleted": "All steps completed", + "processingFailed": "Processing Failed", + "imageProcessingOrTranslationFailed": + "Image processing or translation failed. Please retry.\nError: {error}", + "imageTooLarge": "Image size exceeds 4MB limit", + "imageTooLargeHint": + "Image size exceeds 4MB. Reduce quality or dimensions and retry", + "emptyTranslationResult": "Image translation API returned empty result", + "imageTranslationFailedTryLater": + "Image translation failed. Network issue or rate limit. Please try later", + "imageTranslationApiException": + "Image translation API exception. Check network and retry", + "permissionRequired": "Permission Required", + "cameraPermissionRequired": "Camera permission required to take photos", + "photoAlbumPermissionRequired": + "Photo album permission required to pick images", + "translationDetails": "Translation Details", + "originalImage": "Original Image", + "sourceLanguage": "Source Language", + "targetLanguage": "Target Language", + "translationTime": "Translation Time", + "shareInDevelopment": "Sharing feature under development", + + // 翻译录音文件列表页 "translateAudioFiles": "Translate Audio Files", "noAudioFiles": "No audio files", "calculating": "Calculating...", @@ -1053,7 +1325,7 @@ const Map enUS = { "autoInputCode": "Auto Input Code", "skipBinding": "Skip Binding", "rescan": "Rescan", - "skip": "Skip", + "verifyingDevice": "Verifying device...", "confirmBinding": "Confirm Binding", "associatingDevice": "Associating device...", @@ -1064,7 +1336,7 @@ const Map enUS = { "readyToPair": "Ready to pair", "preparingPairingEnvironment": "Preparing pairing environment...", "connectDeepSoundViaSystemBluetooth": - "Please connect DeepSound device via system Bluetooth first", + "Please connect Voitrans device via system Bluetooth first", "deviceBindingConfirm": "Device binding confirmation...", "establishingDeviceAssociation": "Establishing device association...", "waitingCompletePairing": "Please wait, completing pairing...", @@ -1073,7 +1345,7 @@ const Map enUS = { // 配对错误状态文本 "ensureConnectedDeepSoundDevice": - "Please ensure connected device is DeepSound", + "Please ensure connected device is Voitrans", "deviceAlreadyBoundToOtherAccount": "This device is already bound to another account", "deviceAssociationFailed": "Device association failed, please retry", @@ -1105,7 +1377,7 @@ const Map enUS = { 'retryAuthorization': 'Retry Authorization', 'pleaseGrantNecessaryPermissions': 'Please grant necessary permissions', 'checkFailedRetry': 'Check failed, please retry', - 'pleaseConnectDeepSoundDevice': 'Please connect DeepSound device', + 'pleaseConnectDeepSoundDevice': 'Please connect Voitrans device', 'verificationFailedRetry': 'Verification failed, please retry', 'invalidDeviceAddress': 'Invalid device address', 'bleDeviceAssociationFailed': 'BLE device association failed', @@ -1216,4 +1488,18 @@ const Map enUS = { "justNow": "Just now", "minutesAgo": "@minutes minutes ago", "yesterday": "Yesterday", + // Time extensions (used by picture translation) + "daysAgo": "@days days ago", + "hoursAgo": "@hours hours ago", + + //补充 + "insufficientIntegral": "Insufficient integral", + + //余额不足弹窗 + "insufficientBalanceDesc": + 'Insufficient balance, please go to the corresponding recharge page to recharge.', + "recharge": 'Go to Recharge', + // 会员到期提示(中文注释) + "aiMemberExpiredDesc": + 'AI membership expired, please go to the membership recharge page to renew.', }; diff --git a/lib/core/translations/language/es_es.dart b/lib/core/translations/language/es_es.dart index 6ccc9dae1..76bbcc07a 100644 --- a/lib/core/translations/language/es_es.dart +++ b/lib/core/translations/language/es_es.dart @@ -138,7 +138,8 @@ const Map esES = { "bluetoothInitializationFailure": "Fallo en la inicialización del servicio Bluetooth", "checkingPermissions": "Verificando permisos...", - "lackPermissions": "Faltan permisos de Bluetooth", + "lackPermissions": + "Habilita permisos de Bluetooth y ubicación para descubrir y conectar dispositivos cercanos", "searchingDevice": "Buscando dispositivos...", "findConnectedDevice": "Dispositivo conectado encontrado", "notFound": "No se encontraron dispositivos Bluetooth conectados", @@ -184,22 +185,9 @@ const Map esES = { "conferenceAssistant": "Asistente de reuniones de negocios", "conferenceAssistantInfo": "Grabación de actas en tiempo real y traducción multiidioma", - "studyAssistant": "Asistente de aprendizaje de idiomas", - "studyAssistantInfo": "Corrección de pronunciación y práctica de habla", - "travelTranslator": "Traductor de viajes", - "travelTranslatorInfo": - "Traducción de voz en tiempo real y guía de atracciones turísticas", - "beSearching": "Buscando:", - "noAssistantFound": "No se encontró asistente de IA correspondiente", + "usageStats": "Estadísticas de uso", + "usageStatsInfo": "Consulte las estadísticas de uso de la aplicación", "scene": "Escenarios", - "youHaveSelected": "Has seleccionado:", - "newFunction": "Nuevas funciones", - "youChoseTry": "Elegiste probar", - "aiSinger": "Modo cantante de IA", - "aiSingerInfo": - "Canta cualquier canción con la voz de tu artista favorito, incluso tus propias creaciones", - "popularFunction": "Funciones populares", - "tryNow": "Probar ahora", // 商务会议助手页 "startMeeting": "Iniciar reunión", @@ -215,8 +203,12 @@ const Map esES = { 'meetingTranscript': 'Transcripción de reunión', // 设置页 - "systemSettings": "Configuración del sistema", - "premiumMember": "Miembro premium", + "systemSettings": "Configuración del sistema", //系统设置 + "vipRecharge": "Recarga de VIP", //会员充值 + "aiRecharge": "Miembro premium", //AI充值 + "translationRecharge": "Recarga de traducción", //翻译充值 + "meetingAssistantRecharge": "Recarga de asistente de reunión", //会议助手充值 + "cloudSync": "Recarga de sincronización en la nube", //云端同步 "joinDevice": "Dispositivo conectado:", "accountId": "ID de cuenta:", "systemSetting": "Configuración del sistema", @@ -289,7 +281,7 @@ const Map esES = { //Agent 模块 // agent View - "XiaoYan": "Xiao Yan", + "Lily": "Lily", "clearChatHistory": "Limpiar historial de chat", "online": "En línea", "inputMessage": "Ingresa mensaje...", @@ -316,7 +308,7 @@ const Map esES = { "swipeUpForMore": "Desliza hacia arriba para más", // Agent controller - 'welcomeMessage': '¡Hola! Soy **Xiao Yan**, puedo ayudarte con:\n\n' + 'welcomeMessage': '¡Hola! Soy **Lily**, puedo ayudarte con:\n\n' '• Responder preguntas y proporcionar información\n\n' '• Traducción de voz en tiempo real\n\n' '• Recomendaciones de música y podcasts\n\n' @@ -718,7 +710,7 @@ const Map esES = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Emparejamiento de dispositivo", - "connectYourDeepSoundHeadset": "Conecta tus auriculares DeepSound", + "connectYourDeepSoundHeadset": "Conecta tus auriculares Voitrans", "bind": "Vincular", "authorizeBind": "Autorizar vinculación", "enterCode": "Ingresa código", @@ -779,7 +771,7 @@ const Map esES = { 'retryAuthorization': 'Reintentar autorización', 'pleaseGrantNecessaryPermissions': 'Otorga los permisos necesarios', 'checkFailedRetry': 'Verificación fallida, inténtalo de nuevo', - 'pleaseConnectDeepSoundDevice': 'Conecta dispositivo DeepSound', + 'pleaseConnectDeepSoundDevice': 'Conecta dispositivo Voitrans', 'verificationFailedRetry': 'Verificación fallida, inténtalo de nuevo', 'invalidDeviceAddress': 'Dirección de dispositivo inválida', 'bleDeviceAssociationFailed': 'Fallo en la asociación del dispositivo BLE', @@ -874,4 +866,38 @@ const Map esES = { "乌尔都语": "Urdu", "波斯语": "Persa", "斯瓦希里语": "Suajili", + // Guide overlay keys (placeholders in English) + "sayLilyHint": "Say \"Lily Lily\" to the mic", + "imHere": "I’m here~", + "guideWelcomeWakeAI": "Welcome to Voitrans! First, wake your AI “Lily”.", + "aiReplyCompleted": "Lily has finished the reply!", + "aiThinking": "Lily is thinking…", + "guideAwakenSuccess": "Great! Lily is awake—ask it to help.", + "confirmInput": "Confirm Input", + "guideClickSendPrefix": "Tap send, ", + "guideWeatherStart": "Tell Lily: “What’s the weather today?”", + "guideWeatherInput": "What’s the weather today?", + "guideWeatherSend": "Lily will report the weather in real time", + "guideWeatherEnd": "Lily can report weather live—try other commands", + "guideMusicStart": "Tell Lily: “Play some rock music”", + "guideMusicInput": "Play some rock music", + "guideMusicSend": "Lily will find music for you", + "guideMusicEnd": "Lily found music—try other commands", + "guideNavStart": "Tell Lily: “Navigate to Shenzhen Airport”", + "guideNavInput": "Navigate to Shenzhen Airport", + "guideNavSend": "Lily will plan the route", + "guideNavEnd": "Navigation set—try other commands", + "guideMeetingStart": + "Tell Lily: “Create a meeting reminder for 3pm tomorrow”", + "guideMeetingInput": "Create a meeting reminder for 3pm tomorrow", + "guideMeetingSend": "Lily will create a meeting reminder", + "guideMeetingEnd": "Reminder created—try other commands", + "guidePhoneStart": "Tell Lily: “Call Mom”", + "guidePhoneInput": "Call Mom", + "guidePhoneSend": "Lily will make the call", + "guidePhoneEnd": "Call placed—try other commands", + "guideSmsStart": "Tell Lily: “Send Dad a text: I’ll be home for dinner”", + "guideSmsInput": "Text Dad: I’ll be home for dinner", + "guideSmsSend": "Lily will send the message", + "guideSmsEnd": "Message sent—voice control is convenient", }; diff --git a/lib/core/translations/language/et_ee.dart b/lib/core/translations/language/et_ee.dart index 81fb35c04..ca977d0f2 100644 --- a/lib/core/translations/language/et_ee.dart +++ b/lib/core/translations/language/et_ee.dart @@ -135,7 +135,7 @@ const Map etEE = { "bluetoothInitializationFailure": "Bluetooth teenuse lähtestamine ebaõnnestus", "checkingPermissions": "Lubade kontrollimine...", - "lackPermissions": "Puuduvad Bluetooth load", + "lackPermissions": "Puuduvad Bluetooth lubad ja luba kasutamiseks loetakse", "searchingDevice": "Seadmete otsimine...", "findConnectedDevice": "Leitud ühendatud seade", "notFound": "Ühendatud Bluetooth seadmeid ei leitud", @@ -180,21 +180,9 @@ const Map etEE = { "conferenceAssistant": "Ärikoosoleku assistent", "conferenceAssistantInfo": "Reaalajas protokolli salvestamine ja mitmekeelne tõlge", - "studyAssistant": "Keeleõppe assistent", - "studyAssistantInfo": "Häälduse parandamine ja kõne harjutamine", - "travelTranslator": "Reisi tõlkija", - "travelTranslatorInfo": "Reaalajas häältõlge ja vaatamisväärsuste giid", - "beSearching": "Otsimine:", - "noAssistantFound": "Vastavat AI assistenti ei leitud", + "usageStats": "Kasutamise statistika", + "usageStatsInfo": "Näita sovelluksen kasutamise statistikat", "scene": "Stsenaariumid", - "youHaveSelected": "Oled valinud:", - "newFunction": "Uued funktsioonid", - "youChoseTry": "Valisid proovida", - "aiSinger": "AI laulja režiim", - "aiSingerInfo": - "Laula mis tahes laulu oma lemmikartisti häälega, isegi oma loominguga", - "popularFunction": "Populaarsed funktsioonid", - "tryNow": "Proovi kohe", // 商务会议助手页 "startMeeting": "Alusta koosolekut", @@ -210,8 +198,12 @@ const Map etEE = { 'meetingTranscript': 'Koosoleku transkriptsioon', // 设置页 - "systemSettings": "Süsteemi seaded", - "premiumMember": "Premium liige", + "systemSettings": "Süsteemi seaded", //系统设置 + "vipRecharge": "VIP liige varundus", //会员充值 + "aiRecharge": "Premium liige", //AI充值 + "translationRecharge": "Tõlke varundus", //翻译充值 + "meetingAssistantRecharge": "Koosoleku assistendi varundus", //会议助手充值 + "cloudSync": "Pääsyvaraus cloud-synkronointiin", //云端同步 "joinDevice": "Ühendatud seade:", "accountId": "Konto ID:", "systemSetting": "Süsteemi seaded", @@ -281,7 +273,7 @@ const Map etEE = { //Agent 模块 // agent View - "XiaoYan": "Xiao Yan", + "Lily": "Lily", "clearChatHistory": "Tühjenda vestluse ajalugu", "online": "Võrgus", "inputMessage": "Sisesta sõnum...", @@ -308,7 +300,7 @@ const Map etEE = { "swipeUpForMore": "Libista üles rohkema jaoks", // Agent controller - 'welcomeMessage': 'Tere! Olen **Xiao Yan**, saan sind aidata:\n\n' + 'welcomeMessage': 'Tere! Olen **Lily**, saan sind aidata:\n\n' '• Küsimustele vastamine ja info andmine\n\n' '• Reaalajas häältõlge\n\n' '• Muusika ja podcastide soovitused\n\n' @@ -700,7 +692,7 @@ const Map etEE = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Seadme sidumine", - "connectYourDeepSoundHeadset": "Ühenda oma DeepSound kõrvaklapid", + "connectYourDeepSoundHeadset": "Ühenda oma Voitrans kõrvaklapid", "bind": "Seo", "authorizeBind": "Autoriseeri sidumine", "enterCode": "Sisesta kood", @@ -756,7 +748,7 @@ const Map etEE = { 'retryAuthorization': 'Proovi autoriseerimist uuesti', 'pleaseGrantNecessaryPermissions': 'Anna vajalikud load', 'checkFailedRetry': 'Kontrollimine ebaõnnestus, proovi uuesti', - 'pleaseConnectDeepSoundDevice': 'Ühenda DeepSound seade', + 'pleaseConnectDeepSoundDevice': 'Ühenda Voitrans seade', 'verificationFailedRetry': 'Kontrollimine ebaõnnestus, proovi uuesti', 'invalidDeviceAddress': 'Vigane seadme aadress', 'bleDeviceAssociationFailed': 'BLE seadme seostamine ebaõnnestus', diff --git a/lib/core/translations/language/fa_ir.dart b/lib/core/translations/language/fa_ir.dart index ced6ac8cf..aa577fcbc 100644 --- a/lib/core/translations/language/fa_ir.dart +++ b/lib/core/translations/language/fa_ir.dart @@ -133,7 +133,8 @@ const Map faIR = { "initializing": "در حال آماده‌سازی...", "bluetoothInitializationFailure": "آماده‌سازی سرویس بلوتوث ناموفق بود", "checkingPermissions": "در حال بررسی مجوزها...", - "lackPermissions": "مجوزهای بلوتوث مورد نیاز است", + "lackPermissions": + "مجوزهای بلوتوث مورد نیاز است و دسترسی به لوکیشن نیز لازم است", "searchingDevice": "در حال جستجوی دستگاه‌ها...", "findConnectedDevice": "دستگاه متصل پیدا شد", "notFound": "دستگاه بلوتوث متصلی پیدا نشد", @@ -162,7 +163,7 @@ const Map faIR = { "exploreSlogan": "ایربادهای هوشمند، امکانات بی‌پایان", "exploreSlogan1": "همراه هوشمند، امکانات بی‌پایان", "exploreSlogan2": "عینک هوشمند، امکانات بی‌پایان", - "exploreAi": "کاوش ویژگی‌های هوش مصنوعی ارائه‌شده توسط DeepSound", + "exploreAi": "کاوش ویژگی‌های هوش مصنوعی ارائه‌شده توسط Voitrans", "aiFunction": "ویژگی‌های هوش مصنوعی", "allRoundAssistant": "دستیار همه‌کاره", "allRoundAssistantInfo": "به حل مشکلات مختلف کمک می‌کند", @@ -177,21 +178,9 @@ const Map faIR = { "healthConsultantInfo": "ویژگی‌های بیشتر چت هوش مصنوعی", "conferenceAssistant": "دستیار جلسات کاری", "conferenceAssistantInfo": "ضبط صورت‌جلسه همزمان و ترجمه چندزبانه", - "studyAssistant": "دستیار یادگیری زبان", - "studyAssistantInfo": "تصحیح تلفظ و تمرین مکالمه", - "travelTranslator": "مترجم سفر", - "travelTranslatorInfo": "ترجمه صوتی همزمان و راهنمای جاذبه‌ها", - "beSearching": "در حال جستجو برای:", - "noAssistantFound": "دستیار هوش مصنوعی مورد نظر یافت نشد", + "usageStats": "استفاده از خدمات", + "usageStatsInfo": "دیدن آمار مصرف سرویس‌ها", "scene": "سناریوها", - "youHaveSelected": "شما انتخاب کردید:", - "newFunction": "ویژگی‌های جدید", - "youChoseTry": "شما انتخاب کردید که امتحان کنید", - "aiSinger": "حالت خواننده هوش مصنوعی", - "aiSingerInfo": - "هر آهنگی را با صدای هنرمند مورد علاقه خود بخوانید، حتی ساخته‌های خودتان", - "popularFunction": "ویژگی‌های محبوب", - "tryNow": "اکنون امتحان کنید", // 商务会议助手页 "startMeeting": "شروع جلسه", @@ -428,8 +417,12 @@ const Map faIR = { 'inputAttendees': 'ورود شرکت‌کنندگان', // 设置页 - "systemSettings": "تنظیمات سیستم", - "premiumMember": "عضویت ویژه", + "systemSettings": "تنظیمات سیستم", //系统设置 + "vipRecharge": "شارژ عضویت ویژه", //会员充值 + "aiRecharge": "عضویت ویژه", //AI充值 + "translationRecharge": "شارژ ترجمه", //翻译充值 + "meetingAssistantRecharge": "شارژ کمک‌کننده جلسه", //会议助手充值 + "cloudSync": "شارژ همگام‌سازی ابری", //云端同步 "joinDevice": "دستگاه متصل:", "accountId": "شناسه حساب:", "systemSetting": "تنظیمات سیستم", @@ -498,7 +491,7 @@ const Map faIR = { // Agent模块 //Agent View - "XiaoYan": "شیائویان", + "Lily": "Lily", "clearChatHistory": "پاک کردن تاریخچه چت", "online": "آنلاین", "inputMessage": "پیامی تایپ کنید...", @@ -1035,7 +1028,7 @@ const Map faIR = { "readyToPair": "آماده برای جفت‌سازی", "preparingPairingEnvironment": "در حال آماده‌سازی محیط جفت‌سازی...", "connectDeepSoundViaSystemBluetooth": - "لطفاً ابتدا دستگاه DeepSound را از طریق بلوتوث سیستم متصل کنید", + "لطفاً ابتدا دستگاه Voitrans را از طریق بلوتوث سیستم متصل کنید", "deviceBindingConfirm": "تأیید اتصال دستگاه...", "establishingDeviceAssociation": "در حال برقراری ارتباط دستگاه...", "waitingCompletePairing": "لطفاً صبر کنید، در حال تکمیل جفت‌سازی...", @@ -1044,7 +1037,7 @@ const Map faIR = { // 配对错误状态文本 "ensureConnectedDeepSoundDevice": - "لطفاً اطمینان حاصل کنید که دستگاه متصل DeepSound است", + "لطفاً اطمینان حاصل کنید که دستگاه متصل Voitrans است", "deviceAlreadyBoundToOtherAccount": "این دستگاه قبلاً به حساب دیگری متصل شده است", "deviceAssociationFailed": "ارتباط دستگاه ناموفق بود، لطفاً دوباره تلاش کنید", @@ -1071,7 +1064,7 @@ const Map faIR = { 'retryAuthorization': 'تلاش مجدد برای مجوز', 'pleaseGrantNecessaryPermissions': 'لطفاً مجوزهای لازم را اعطا کنید', 'checkFailedRetry': 'بررسی ناموفق بود، لطفاً دوباره تلاش کنید', - 'pleaseConnectDeepSoundDevice': 'لطفاً دستگاه DeepSound را متصل کنید', + 'pleaseConnectDeepSoundDevice': 'لطفاً دستگاه Voitrans را متصل کنید', 'verificationFailedRetry': 'تأیید ناموفق بود، لطفاً دوباره تلاش کنید', 'invalidDeviceAddress': 'آدرس دستگاه نامعتبر است', 'bleDeviceAssociationFailed': 'ارتباط دستگاه BLE ناموفق بود', diff --git a/lib/core/translations/language/fi_fi.dart b/lib/core/translations/language/fi_fi.dart index e9bd336fd..b1d4c535a 100644 --- a/lib/core/translations/language/fi_fi.dart +++ b/lib/core/translations/language/fi_fi.dart @@ -136,7 +136,8 @@ const Map fiFI = { "initializing": "Alustetaan...", "bluetoothInitializationFailure": "Bluetooth-palvelun alustus epäonnistui", "checkingPermissions": "Tarkistetaan käyttöoikeuksia...", - "lackPermissions": "Bluetooth-käyttöoikeudet puuttuvat", + "lackPermissions": + "Bluetooth-käyttöoikeudet puuttuvat ja lokaatioon pääsy on tarpeen", "searchingDevice": "Etsitään laitteita...", "findConnectedDevice": "Yhdistetty laite löytyi", "notFound": "Yhdistettyä Bluetooth-laitetta ei löytynyt", @@ -181,22 +182,9 @@ const Map fiFI = { "conferenceAssistant": "Yrityskokousavustaja", "conferenceAssistantInfo": "Reaaliaikainen pöytäkirjan pito, monikielinen käännös", - "studyAssistant": "Kielenoppimisavustaja", - "studyAssistantInfo": "Ääntämisen korjaus, reaaliaikainen puheharjoittelu", - "travelTranslator": "Matkakäännösavustaja", - "travelTranslatorInfo": - "Reaaliaikainen puhekäännös, matkailukohteiden esittely", - "beSearching": "Etsitään:", - "noAssistantFound": "Vastaavaa AI-avustajaa ei löytynyt", + "usageStats": "Käyttöstatistiikka", + "usageStatsInfo": "Näytä sovelluksen käytön statistiikka", "scene": "Skenaario", - "youHaveSelected": "Olet valinnut:", - "newFunction": "Uusi toiminto", - "youChoseTry": "Valitsit kokeilla", - "aiSinger": "AI-laulaja-tila", - "aiSingerInfo": - "Laula mikä tahansa kappale suosikkilaulajasi äänellä, jopa omat sävellyksesi", - "popularFunction": "Suositut toimintoskenaariot", - "tryNow": "Kokeile nyt", // 商务会议助手页 "startMeeting": "Aloita kokous", @@ -212,8 +200,12 @@ const Map fiFI = { 'meetingTranscript': 'Kokouksen transkriptio', // 设置页 - "systemSettings": "Järjestelmäasetukset", - "premiumMember": "Premium-jäsen", + "systemSettings": "Järjestelmäasetukset", //系统设置 + "vipRecharge": "Premium-jäsenvaraus", //会员充值 + "aiRecharge": "Premium-jäsen", //AI充值 + "translationRecharge": "Käännösvaraus", //翻译充值 + "meetingAssistantRecharge": "Kokousavustajan varaus", //会议助手充值 + "cloudSync": "Pääsyvaraus cloud-synkronointiin", //云端同步 "joinDevice": "Yhdistetty laite:", "accountId": "Tilin tunnus:", "systemSetting": "Järjestelmäasetukset", @@ -283,7 +275,7 @@ const Map fiFI = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Tyhjennä keskusteluhistoria", "online": "Online", "inputMessage": "Kirjoita viesti...", @@ -310,7 +302,7 @@ const Map fiFI = { "swipeUpForMore": "Pyyhkäise ylös nähdäksesi lisää", // Agent controller - 'welcomeMessage': 'Hei! Olen **XiaoYan**, voin auttaa sinua:\n\n' + 'welcomeMessage': 'Hei! Olen **Lily**, voin auttaa sinua:\n\n' '• Vastaamaan kysymyksiin ja antamaan tietoa\n\n' '• Reaaliaikaisessa puhekäännöksessä\n\n' '• Suosittelemaan musiikkia ja podcasteja\n\n' @@ -697,7 +689,7 @@ const Map fiFI = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Laitteen paritus", - "connectYourDeepSoundHeadset": "Yhdistä DeepSound-kuulokkeesi", + "connectYourDeepSoundHeadset": "Yhdistä Voitrans-kuulokkeesi", "bind": "Sido", "authorizeBind": "Valtuuta sidonta", "enterCode": "Anna koodi", @@ -715,7 +707,7 @@ const Map fiFI = { "readyToPair": "Valmis paritukseen", "preparingPairingEnvironment": "Valmistellaan paritusympäristöä...", "connectDeepSoundViaSystemBluetooth": - "Yhdistä ensin DeepSound-laitteeseen järjestelmän Bluetoothin kautta", + "Yhdistä ensin Voitrans-laitteeseen järjestelmän Bluetoothin kautta", "deviceBindingConfirm": "Laitteen sidonnan vahvistus...", "establishingDeviceAssociation": "Luodaan laiteyhteyttä...", "waitingCompletePairing": "Odota, paritus on valmis...", @@ -750,7 +742,7 @@ const Map fiFI = { 'retryAuthorization': 'Yritä valtuutusta uudelleen', 'pleaseGrantNecessaryPermissions': 'Myönnä tarvittavat käyttöoikeudet', 'checkFailedRetry': 'Tarkistus epäonnistui, yritä uudelleen', - 'pleaseConnectDeepSoundDevice': 'Yhdistä DeepSound-laite', + 'pleaseConnectDeepSoundDevice': 'Yhdistä Voitrans-laite', 'verificationFailedRetry': 'Vahvistus epäonnistui, yritä uudelleen', 'invalidDeviceAddress': 'Virheellinen laiteosoite', 'bleDeviceAssociationFailed': 'BLE-laiteyhteyden luominen epäonnistui', diff --git a/lib/core/translations/language/fr_fr.dart b/lib/core/translations/language/fr_fr.dart index ad0f7c7a7..079135fa0 100644 --- a/lib/core/translations/language/fr_fr.dart +++ b/lib/core/translations/language/fr_fr.dart @@ -139,7 +139,8 @@ const Map frFR = { "bluetoothInitializationFailure": "Échec de l'initialisation du service Bluetooth", "checkingPermissions": "Vérification des autorisations...", - "lackPermissions": "Autorisations Bluetooth requises", + "lackPermissions": + "Autorisations Bluetooth requises et accès à la localisation nécessaire", "searchingDevice": "Recherche d'appareils...", "findConnectedDevice": "Appareil connecté trouvé", "notFound": "Aucun appareil Bluetooth connecté trouvé", @@ -186,23 +187,9 @@ const Map frFR = { "conferenceAssistant": "Assistant de réunion d'affaires", "conferenceAssistantInfo": "Enregistrement de procès-verbaux en temps réel et traduction multilingue", - "studyAssistant": "Assistant d'apprentissage des langues", - "studyAssistantInfo": - "Correction de prononciation et pratique de l'expression orale", - "travelTranslator": "Traducteur de voyage", - "travelTranslatorInfo": - "Traduction vocale en temps réel et guides d'attractions", - "beSearching": "Recherche de :", - "noAssistantFound": "Aucun assistant IA correspondant trouvé", + "usageStats": "Statistiques d'utilisation", + "usageStatsInfo": "Consultez les statistiques d'utilisation de l'application", "scene": "Scénarios", - "youHaveSelected": "Vous avez sélectionné :", - "newFunction": "Nouvelles fonctions", - "youChoseTry": "Vous avez choisi d'essayer", - "aiSinger": "Mode chanteur IA", - "aiSingerInfo": - "Chantez n'importe quelle chanson avec la voix de votre artiste préféré, même vos propres créations", - "popularFunction": "Fonctions populaires", - "tryNow": "Essayer maintenant", // 商务会议助手页 "startMeeting": "Démarrer la réunion", @@ -218,8 +205,12 @@ const Map frFR = { 'meetingTranscript': 'Transcription de la réunion', // 设置页 - "systemSettings": "Paramètres système", - "premiumMember": "Membre premium", + "systemSettings": "Paramètres système", //系统设置 + "vipRecharge": "Recharge de VIP", //会员充值 + "aiRecharge": "Membre premium", //AI充值 + "translationRecharge": "Recharge de traduction", //翻译充值 + "meetingAssistantRecharge": "Recharge de l'assistant de réunion", //会议助手充值 + "cloudSync": "Recharge de synchronisation cloud", //云端同步 "joinDevice": "Appareil connecté :", "accountId": "ID du compte :", "systemSetting": "Paramètres système", @@ -292,7 +283,7 @@ const Map frFR = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Effacer l'historique de chat", "online": "En ligne", "inputMessage": "Saisir un message...", @@ -320,14 +311,13 @@ const Map frFR = { "swipeUpForMore": "Glisser vers le haut pour plus", // Agent controller - 'welcomeMessage': - 'Bonjour ! Je suis **XiaoYan**, je peux vous aider avec :\n\n' - '• Répondre aux questions et fournir des informations\n\n' - '• Traduction vocale en temps réel\n\n' - '• Recommander de la musique et des podcasts\n\n' - '• Mémos vocaux et rappels\n\n' - '• Contrôler les fonctions des écouteurs\n\n' - 'Comment puis-je vous aider ?', + 'welcomeMessage': 'Bonjour ! Je suis **Lily**, je peux vous aider avec :\n\n' + '• Répondre aux questions et fournir des informations\n\n' + '• Traduction vocale en temps réel\n\n' + '• Recommander de la musique et des podcasts\n\n' + '• Mémos vocaux et rappels\n\n' + '• Contrôler les fonctions des écouteurs\n\n' + 'Comment puis-je vous aider ?', 'microphonePermission': 'Microphone', 'voiceRecognitionPermissionText': 'La fonction de reconnaissance vocale nécessite l\'accès au microphone.', @@ -724,7 +714,7 @@ const Map frFR = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Appairage d'appareil", - "connectYourDeepSoundHeadset": "Connectez votre casque DeepSound", + "connectYourDeepSoundHeadset": "Connectez votre casque Voitrans", "bind": "Lier", "authorizeBind": "Autoriser la liaison", "enterCode": "Veuillez saisir le code", @@ -743,7 +733,7 @@ const Map frFR = { "preparingPairingEnvironment": "Préparation de l'environnement d'appairage...", "connectDeepSoundViaSystemBluetooth": - "Veuillez d'abord connecter l'appareil DeepSound via le Bluetooth système", + "Veuillez d'abord connecter l'appareil Voitrans via le Bluetooth système", "deviceBindingConfirm": "Confirmation de liaison d'appareil...", "establishingDeviceAssociation": "Établissement de l'association d'appareil...", @@ -753,7 +743,7 @@ const Map frFR = { // 配对错误状态文本 "ensureConnectedDeepSoundDevice": - "Veuillez vous assurer que l'appareil connecté est DeepSound", + "Veuillez vous assurer que l'appareil connecté est Voitrans", "deviceAlreadyBoundToOtherAccount": "Cet appareil est déjà lié à un autre compte", "deviceAssociationFailed": @@ -788,7 +778,7 @@ const Map frFR = { 'pleaseGrantNecessaryPermissions': 'Veuillez accorder les autorisations nécessaires', 'checkFailedRetry': 'Échec de la vérification, veuillez réessayer', - 'pleaseConnectDeepSoundDevice': 'Veuillez connecter l\'appareil DeepSound', + 'pleaseConnectDeepSoundDevice': 'Veuillez connecter l\'appareil Voitrans', 'verificationFailedRetry': 'Échec de la vérification, veuillez réessayer', 'invalidDeviceAddress': 'Adresse d\'appareil invalide', 'bleDeviceAssociationFailed': 'Échec de l\'association d\'appareil BLE', @@ -883,4 +873,38 @@ const Map frFR = { "乌尔都语": "Ourdou", "波斯语": "Persan", "斯瓦希里语": "Swahili", + // Guide overlay keys (placeholders in English) + "sayLilyHint": "Say \"Lily Lily\" to the mic", + "imHere": "I’m here~", + "guideWelcomeWakeAI": "Welcome to Voitrans! First, wake your AI “Lily”.", + "aiReplyCompleted": "Lily has finished the reply!", + "aiThinking": "Lily is thinking…", + "guideAwakenSuccess": "Great! Lily is awake—ask it to help.", + "confirmInput": "Confirm Input", + "guideClickSendPrefix": "Tap send, ", + "guideWeatherStart": "Tell Lily: “What’s the weather today?”", + "guideWeatherInput": "What’s the weather today?", + "guideWeatherSend": "Lily will report the weather in real time", + "guideWeatherEnd": "Lily can report weather live—try other commands", + "guideMusicStart": "Tell Lily: “Play some rock music”", + "guideMusicInput": "Play some rock music", + "guideMusicSend": "Lily will find music for you", + "guideMusicEnd": "Lily found music—try other commands", + "guideNavStart": "Tell Lily: “Navigate to Shenzhen Airport”", + "guideNavInput": "Navigate to Shenzhen Airport", + "guideNavSend": "Lily will plan the route", + "guideNavEnd": "Navigation set—try other commands", + "guideMeetingStart": + "Tell Lily: “Create a meeting reminder for 3pm tomorrow”", + "guideMeetingInput": "Create a meeting reminder for 3pm tomorrow", + "guideMeetingSend": "Lily will create a meeting reminder", + "guideMeetingEnd": "Reminder created—try other commands", + "guidePhoneStart": "Tell Lily: “Call Mom”", + "guidePhoneInput": "Call Mom", + "guidePhoneSend": "Lily will make the call", + "guidePhoneEnd": "Call placed—try other commands", + "guideSmsStart": "Tell Lily: “Send Dad a text: I’ll be home for dinner”", + "guideSmsInput": "Text Dad: I’ll be home for dinner", + "guideSmsSend": "Lily will send the message", + "guideSmsEnd": "Message sent—voice control is convenient", }; diff --git a/lib/core/translations/language/hi_in.dart b/lib/core/translations/language/hi_in.dart index 58c9c48bb..187bc6c3e 100644 --- a/lib/core/translations/language/hi_in.dart +++ b/lib/core/translations/language/hi_in.dart @@ -134,7 +134,8 @@ const Map hiIN = { "initializing": "प्रारंभ हो रहा है...", "bluetoothInitializationFailure": "ब्लूटूथ सेवा प्रारंभ करने में विफल", "checkingPermissions": "अनुमतियों की जाँच हो रही है...", - "lackPermissions": "ब्लूटूथ अनुमतियों की कमी है", + "lackPermissions": + "ब्लूटूथ अनुमतियों की कमी है और लोकेशन तक पहुंच नहीं सकता है", "searchingDevice": "डिवाइस खोज रहा है...", "findConnectedDevice": "कनेक्टेड डिवाइस मिला", "notFound": "कोई कनेक्टेड ब्लूटूथ डिवाइस नहीं मिला", @@ -179,21 +180,9 @@ const Map hiIN = { "conferenceAssistant": "बिजनेस मीटिंग असिस्टेंट", "conferenceAssistantInfo": "रीयल-टाइम मीटिंग मिनट रिकॉर्डिंग, बहु-भाषा अनुवाद", - "studyAssistant": "भाषा सीखने का सहायक", - "studyAssistantInfo": "उच्चारण सुधार, रीयल-टाइम बोलने का अभ्यास", - "travelTranslator": "यात्रा अनुवाद सहायक", - "travelTranslatorInfo": "रीयल-टाइम वॉयस अनुवाद, पर्यटन स्थलों का परिचय", - "beSearching": "खोज रहा है:", - "noAssistantFound": "कोई संबंधित एआई सहायक नहीं मिला", + "usageStats": "उपयोग सtatistik", + "usageStatsInfo": "अप्लिकेशन का उपयोग सtatistik देखें", "scene": "दृश्य", - "youHaveSelected": "आपने चुना है:", - "newFunction": "नया फ़ंक्शन", - "youChoseTry": "आपने कोशिश करने के लिए चुना", - "aiSinger": "एआई सिंगर मोड", - "aiSingerInfo": - "अपने पसंदीदा गायक की आवाज में कोई भी गाना गाएं, यहां तक कि अपनी खुद की रचनाएं भी", - "popularFunction": "लोकप्रिय फ़ंक्शन दृश्य", - "tryNow": "अभी চেষ্টা করুন", // 商务会议助手页 "startMeeting": "बैठक शुरू करें", @@ -209,8 +198,12 @@ const Map hiIN = { 'meetingTranscript': 'बैठक का प्रतिलेख', // 设置页 - "systemSettings": "सिस्टम सेटिंग्स", - "premiumMember": "प्रीमियम सदस्य", + "systemSettings": "सिस्टम सेटिंग्स", //系统设置 + "vipRecharge": "प्रीमियम सदस्य रिचार्ज", //会员充值 + "aiRecharge": "प्रीमियम सदस्य", //AI充值 + "translationRecharge": "अनुवाद रिचार्ज", //翻译充值 + "meetingAssistantRecharge": "बैठक असिस्टेंट रिचार्ज", //会议助手充值 + "cloudSync": "क्लाउड सिंक्रोनाइज़ेशन रिचार्ज", //云端同步 "joinDevice": "कनेक्टेड डिवाइस:", "accountId": "खाता आईडी:", "systemSetting": "सिस्टम सेटिंग", @@ -279,7 +272,7 @@ const Map hiIN = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "चैट इतिहास साफ़ करें", "online": "ऑनलाइन", "inputMessage": "संदेश दर्ज करें...", @@ -307,7 +300,7 @@ const Map hiIN = { "swipeUpForMore": "अधिक के लिए ऊपर स्वाइप करें", // Agent controller - 'welcomeMessage': 'नमस्ते! मैं **XiaoYan** हूँ, मैं आपकी मदद कर सकता हूँ:\n\n' + 'welcomeMessage': 'नमस्ते! मैं **Lily** हूँ, मैं आपकी मदद कर सकता हूँ:\n\n' '• सवालों के जवाब देना और जानकारी प्रदान करना\n\n' '• रीयल-टाइम वॉयस अनुवाद\n\n' '• संगीत और पॉडकास्ट की सिफारिश करना\n\n' @@ -698,7 +691,7 @@ const Map hiIN = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "डिवाइस पेयरिंग", - "connectYourDeepSoundHeadset": "अपना DeepSound हेडसेट कनेक्ट करें", + "connectYourDeepSoundHeadset": "अपना Voitrans हेडसेट कनेक्ट करें", "bind": "बाँधें", "authorizeBind": "बाँधने को अधिकृत करें", "enterCode": "कृपया कोड दर्ज करें", @@ -753,7 +746,7 @@ const Map hiIN = { 'retryAuthorization': 'प्राधिकरण पुनः प्रयास करें', 'pleaseGrantNecessaryPermissions': 'कृपया आवश्यक अनुमतियाँ दें', 'checkFailedRetry': 'जाँच विफल, कृपया पुनः प्रयास करें', - 'pleaseConnectDeepSoundDevice': 'कृपया एक DeepSound डिवाइस कनेक्ट करें', + 'pleaseConnectDeepSoundDevice': 'कृपया एक Voitrans डिवाइस कनेक्ट करें', 'verificationFailedRetry': 'सत्यापन विफल, कृपया पुनः प्रयास करें', 'invalidDeviceAddress': 'अमान्य डिवाइस पता', 'bleDeviceAssociationFailed': 'BLE डिवाइस एसोसिएशन विफल', @@ -848,4 +841,109 @@ const Map hiIN = { "乌尔都语": "उर्दू", "波斯语": "फारसी", "斯瓦希里语": "स्वाहिली", + // Guide overlay keys (placeholders in English) + "sayLilyHint": "Say \"Lily Lily\" to the mic", + "imHere": "I’m here~", + "guideWelcomeWakeAI": "Welcome to Voitrans! First, wake your AI “Lily”.", + "aiReplyCompleted": "Lily has finished the reply!", + "aiThinking": "Lily is thinking…", + "guideAwakenSuccess": "Great! Lily is awake—ask it to help.", + "confirmInput": "Confirm Input", + "guideClickSendPrefix": "Tap send, ", + "guideWeatherStart": "Tell Lily: “What’s the weather today?”", + "guideWeatherInput": "What’s the weather today?", + "guideWeatherSend": "Lily will report the weather in real time", + "guideWeatherEnd": "Lily can report weather live—try other commands", + "guideMusicStart": "Tell Lily: “Play some rock music”", + "guideMusicInput": "Play some rock music", + "guideMusicSend": "Lily will find music for you", + "guideMusicEnd": "Lily found music—try other commands", + "guideNavStart": "Tell Lily: “Navigate to Shenzhen Airport”", + "guideNavInput": "Navigate to Shenzhen Airport", + "guideNavSend": "Lily will plan the route", + "guideNavEnd": "Navigation set—try other commands", + "guideMeetingStart": + "Tell Lily: “Create a meeting reminder for 3pm tomorrow”", + "guideMeetingInput": "Create a meeting reminder for 3pm tomorrow", + "guideMeetingSend": "Lily will create a meeting reminder", + "guideMeetingEnd": "Reminder created—try other commands", + "guidePhoneStart": "Tell Lily: “Call Mom”", + "guidePhoneInput": "Call Mom", + "guidePhoneSend": "Lily will make the call", + "guidePhoneEnd": "Call placed—try other commands", + "guideSmsStart": "Tell Lily: “Send Dad a text: I’ll be home for dinner”", + "guideSmsInput": "Text Dad: I’ll be home for dinner", + "guideSmsSend": "Lily will send the message", + "guideSmsEnd": "Message sent—voice control is convenient", + // Goods pages common footer (placeholders) + 'appNameProvider': 'App: VoiTrans · Provided by: VoiTrans', + 'copyrightVoitrans': '© 2025 Voitrans Technologies', + + // Meeting goods page (placeholders) + 'meetingHeaderDesc1': + 'Audio recording, transcription, keyword search, and auto summary.', + 'meetingHeaderDesc2': + 'Suitable for business meetings, class notes, and interview minutes.', + 'meetingAutoRecording': 'Auto recording and segmentation', + 'meetingSpeakerTagging': 'Speaker labeling', + 'meetingLongAudioTranscription': 'Long audio transcription', + 'meetingKeyPointSummary': 'Key point extraction', + 'meetingTopicNameSearch': 'Search by topic/name', + 'meetingTimelineJump': 'Timeline jump', + 'meetingExportSummary': 'Export minutes', + 'meetingMultiPlatformShare': 'Share across platforms', + 'meetingDisclaimer': + 'Use in quiet environments; performance depends on microphone and network.', + + // Translation goods page (placeholders) + 'transHeaderDesc1': + 'रीयल-टाइम समकालीन व्याख्या और पाठ अनुवाद, कम विलंबता वॉइस प्रसारण के साथ।', + 'transHeaderDesc2': 'यात्रा, व्यवसाय, भाषण और शिक्षण के लिए उपयुक्त।', + 'translateZhEn': 'चीनी-अंग्रेज़ी अनुवाद', + 'lowLatencyVoiceBroadcast': 'कम विलंबता वॉइस प्रसारण', + 'multiLangTextTranslation': 'बहुभाषी पाठ अनुवाद', + 'termOptimizationContextPreserve': + 'पारिभाषिक शब्दों का अनुकूलन और संदर्भ बनाए रखना', + 'lexiconTourBusinessLife': 'यात्रा/व्यवसाय/जीवन शब्दकोश', + 'personalVocabularyMemory': 'व्यक्तिगत शब्दावली स्मृति', + 'conversationSubtitleLargeText': 'वार्तालाप उपशीर्षक / बड़े-टेक्स्ट मोड', + 'colorHighContrast': 'उच्च रंग-विपरीत', + 'transDisclaimer': + 'अनुवाद की गुणवत्ता माइक्रोफोन, नेटवर्क विलंबता और परिवेशी शोर पर निर्भर करती है।', + + // VIP goods page (placeholders) + 'benefitsOverview': 'लाभों का अवलोकन', + 'higherQuota': 'उच्च उपयोग कोटा', + 'lowerUnitPrice': 'कम इकाई मूल्य', + 'priorityService': 'प्राथमिकता सेवा', + 'exclusiveSupportChannel': 'समर्पित सहायता चैनल', + 'fastResponse': 'समस्याओं पर तेज़ प्रतिक्रिया', + 'eventsAndDiscounts': 'इवेंट और छूट', + 'memberExclusiveEvents': 'सदस्य-विशेष इवेंट', + 'longTermDiscountPlan': 'दीर्घकालीन छूट योजना', + 'generalPrivileges': 'सामान्य विशेषाधिकार', + 'earlyAccessNewFeatures': 'नई सुविधाओं का प्रारंभिक उपयोग', + 'crossDeviceSync': 'क्रॉस-डिवाइस सिंक', + 'vipDisclaimer': + 'वास्तविक लाभ पैकेज विवरण पर निर्भर करते हैं और संस्करणों के साथ बदल सकते हैं।', + // Goods names by id + 'ai_001_name': 'एआई बेसिक 100', + 'ai_002_name': 'एआई बेसिक 500', + 'ai_003_name': 'एआई बेसिक 1000', + 'meet_001_name': 'मीटिंग बेसिक 100', + 'meet_002_name': 'मीटिंग बेसिक 500', + 'meet_003_name': 'मीटिंग बेसिक 1000', + 'trans_001_name': 'अनुवाद बेसिक 100', + 'trans_002_name': 'अनुवाद बेसिक 500', + 'trans_003_name': 'अनुवाद बेसिक 1000', + // Goods descriptions by id + 'ai_001_desc': '100 उपयोग शामिल। मुख्य AI सुविधाओं की पहुँच।', + 'ai_002_desc': '500 उपयोग शामिल। मुख्य AI सुविधाओं की पहुँच।', + 'ai_003_desc': '1000 उपयोग शामिल। मुख्य AI सुविधाओं की पहुँच।', + 'meet_001_desc': '100 बैठक रिकॉर्डिंग/ट्रांसक्रिप्शन शामिल।', + 'meet_002_desc': '500 बैठक रिकॉर्डिंग/ट्रांसक्रिप्शन शामिल।', + 'meet_003_desc': '1000 बैठक रिकॉर्डिंग/ट्रांसक्रिप्शन शामिल।', + 'trans_001_desc': '100 अनुवाद (वॉयस/टेक्स्ट) शामिल।', + 'trans_002_desc': '500 अनुवाद (वॉयस/टेक्स्ट) शामिल।', + 'trans_003_desc': '1000 अनुवाद (वॉयस/टेक्स्ट) शामिल।', }; diff --git a/lib/core/translations/language/hr_hr.dart b/lib/core/translations/language/hr_hr.dart index bd42f68a5..a98916deb 100644 --- a/lib/core/translations/language/hr_hr.dart +++ b/lib/core/translations/language/hr_hr.dart @@ -137,7 +137,8 @@ const Map hrHR = { "bluetoothInitializationFailure": "Inicijalizacija Bluetooth usluge nije uspjela", "checkingPermissions": "Provjera dopuštenja...", - "lackPermissions": "Nedostaju Bluetooth dopuštenja", + "lackPermissions": + "Nedostaju Bluetooth dopuštenja i pristup lokacijskom podacima je potrebno", "searchingDevice": "Pretraživanje uređaja...", "findConnectedDevice": "Pronađen povezan uređaj", "notFound": "Nije pronađen povezan Bluetooth uređaj", @@ -183,23 +184,9 @@ const Map hrHR = { "conferenceAssistant": "Asistent za poslovne sastanke", "conferenceAssistantInfo": "Bilježenje zapisnika u stvarnom vremenu, višejezični prijevod", - "studyAssistant": "Asistent za učenje jezika", - "studyAssistantInfo": - "Ispravak izgovora, vježbanje govora u stvarnom vremenu", - "travelTranslator": "Asistent za prevođenje na putovanjima", - "travelTranslatorInfo": - "Glasovni prijevod u stvarnom vremenu, predstavljanje turističkih atrakcija", - "beSearching": "Traženje:", - "noAssistantFound": "Nije pronađen odgovarajući AI asistent", + "usageStats": "Statistika upotrebe", + "usageStatsInfo": "Pregledajte statistiku upotrebe aplikacije", "scene": "Scenarij", - "youHaveSelected": "Odabrali ste:", - "newFunction": "Nova funkcija", - "youChoseTry": "Odabrali ste isprobati", - "aiSinger": "AI pjevač način rada", - "aiSingerInfo": - "Pjevajte bilo koju pjesmu glasom svog omiljenog pjevača, čak i vlastite skladbe", - "popularFunction": "Popularni scenariji funkcija", - "tryNow": "Isprobaj sada", // 商务会议助手页 "startMeeting": "Započni sastanak", @@ -215,8 +202,12 @@ const Map hrHR = { 'meetingTranscript': 'Transkript sastanka', // 设置页 - "systemSettings": "Postavke sustava", - "premiumMember": "Premium član", + "systemSettings": "Postavke sustava", //系统设置 + "vipRecharge": "Ponuda premium članstva", //会员充值 + "aiRecharge": "Premium član", //AI充值 + "translationRecharge": "Prijevod ponuda", //翻译充值 + "meetingAssistantRecharge": "Asistent sastanka ponuda", //会议助手充值 + "cloudSync": "Synchronizacija u oblaku ponuda", //云端同步 "joinDevice": "Povezan uređaj:", "accountId": "ID računa:", "systemSetting": "Postavke sustava", @@ -285,7 +276,7 @@ const Map hrHR = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Obriši povijest razgovora", "online": "Na mreži", "inputMessage": "Unesite poruku...", @@ -312,7 +303,7 @@ const Map hrHR = { "swipeUpForMore": "Prijeđite prstom prema gore za više", // Agent controller - 'welcomeMessage': 'Zdravo! Ja sam **XiaoYan**, mogu vam pomoći:\n\n' + 'welcomeMessage': 'Zdravo! Ja sam **Lily**, mogu vam pomoći:\n\n' '• Odgovarati na pitanja i pružati informacije\n\n' '• Glasovno prevođenje u stvarnom vremenu\n\n' '• Preporučivati glazbu i podcaste\n\n' @@ -702,7 +693,7 @@ const Map hrHR = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Uparivanje uređaja", - "connectYourDeepSoundHeadset": "Povežite svoje DeepSound slušalice", + "connectYourDeepSoundHeadset": "Povežite svoje Voitrans slušalice", "bind": "Poveži", "authorizeBind": "Autoriziraj povezivanje", "enterCode": "Unesite kod", diff --git a/lib/core/translations/language/hu_hu.dart b/lib/core/translations/language/hu_hu.dart index b503a87c7..375f8a3f2 100644 --- a/lib/core/translations/language/hu_hu.dart +++ b/lib/core/translations/language/hu_hu.dart @@ -139,7 +139,8 @@ const Map huHU = { "bluetoothInitializationFailure": "A Bluetooth szolgáltatás inicializálása sikertelen", "checkingPermissions": "Engedélyek ellenőrzése...", - "lackPermissions": "Hiányoznak a Bluetooth engedélyek", + "lackPermissions": + "Hiányoznak a Bluetooth engedélyek és a helyzetek elérése szükséges", "searchingDevice": "Eszközök keresése...", "findConnectedDevice": "Csatlakoztatott eszköz található", "notFound": "Nem található csatlakoztatott Bluetooth eszköz", @@ -184,22 +185,9 @@ const Map huHU = { "healthConsultantInfo": "További AI csevegési funkciókat kínál", "conferenceAssistant": "Üzleti megbeszélés asszisztens", "conferenceAssistantInfo": "Valós idejű jegyzőkönyvezés, többnyelvű fordítás", - "studyAssistant": "Nyelvtanulási asszisztens", - "studyAssistantInfo": "Kiejtésjavítás, valós idejű beszédgyakorlás", - "travelTranslator": "Utazási fordító asszisztens", - "travelTranslatorInfo": - "Valós idejű hangfordítás, turisztikai látványosságok bemutatása", - "beSearching": "Keresés:", - "noAssistantFound": "Nem található megfelelő AI asszisztens", + "usageStats": "Használati statisztika", + "usageStatsInfo": "Lásd a használati statisztikát", "scene": "Helyzet", - "youHaveSelected": "Ön választotta:", - "newFunction": "Új funkció", - "youChoseTry": "Ön a próbát választotta", - "aiSinger": "AI énekes mód", - "aiSingerInfo": - "Énekeljen el bármilyen dalt kedvenc énekese hangján, akár saját szerzeményeit is", - "popularFunction": "Népszerű funkciók", - "tryNow": "Próbálja ki most", // 商务会议助手页 "startMeeting": "Megbeszélés indítása", @@ -215,8 +203,12 @@ const Map huHU = { 'meetingTranscript': 'Megbeszélés átirata', // 设置页 - "systemSettings": "Rendszerbeállítások", - "premiumMember": "Prémium tag", + "systemSettings": "Rendszerbeállítások", //系统设置 + "vipRecharge": "Prémium tag ponuda", //会员充值 + "aiRecharge": "Prémium tag", //AI充值 + "translationRecharge": "Fordítás ponuda", //翻译充值 + "meetingAssistantRecharge": "Megbeszélés asszisztens ponuda", //会议助手充值 + "cloudSync": "Synchronizációs ponuda", //云端同步 "joinDevice": "Csatlakoztatott eszköz:", "accountId": "Fiók azonosító:", "systemSetting": "Rendszerbeállítás", @@ -285,7 +277,7 @@ const Map huHU = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Csevegési előzmények törlése", "online": "Online", "inputMessage": "Üzenet beírása...", @@ -313,7 +305,7 @@ const Map huHU = { "swipeUpForMore": "Húzza felfelé a továbbiakért", // Agent controller - 'welcomeMessage': 'Szia! **XiaoYan** vagyok, segíthetek neked:\n\n' + 'welcomeMessage': 'Szia! **Lily** vagyok, segíthetek neked:\n\n' '• Kérdések megválaszolásában és információk nyújtásában\n\n' '• Valós idejű hangfordításban\n\n' '• Zene és podcastok ajánlásában\n\n' @@ -701,7 +693,7 @@ const Map huHU = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Eszköz párosítása", - "connectYourDeepSoundHeadset": "Csatlakoztassa a DeepSound fülhallgatóját", + "connectYourDeepSoundHeadset": "Csatlakoztassa a Voitrans fülhallgatóját", "bind": "Párosítás", "authorizeBind": "Párosítás engedélyezése", "enterCode": "Kérjük, adja meg a kódot", @@ -757,7 +749,7 @@ const Map huHU = { 'Kérjük, adja meg a szükséges engedélyeket', 'checkFailedRetry': 'Az ellenőrzés sikertelen, próbálja újra', 'pleaseConnectDeepSoundDevice': - 'Kérjük, csatlakoztasson egy DeepSound eszközt', + 'Kérjük, csatlakoztasson egy Voitrans eszközt', 'verificationFailedRetry': 'Az ellenőrzés sikertelen, próbálja újra', 'invalidDeviceAddress': 'Érvénytelen eszközcím', 'bleDeviceAssociationFailed': 'A BLE eszköztársítás sikertelen', diff --git a/lib/core/translations/language/id_id.dart b/lib/core/translations/language/id_id.dart index 50ee06329..5fc4c3983 100644 --- a/lib/core/translations/language/id_id.dart +++ b/lib/core/translations/language/id_id.dart @@ -135,7 +135,7 @@ const Map idID = { "initializing": "Menginisialisasi...", "bluetoothInitializationFailure": "Inisialisasi layanan Bluetooth gagal", "checkingPermissions": "Memeriksa izin...", - "lackPermissions": "Izin Bluetooth tidak ada", + "lackPermissions": "Izin Bluetooth tidak ada dan akses lokasi diperlukan", "searchingDevice": "Mencari perangkat...", "findConnectedDevice": "Perangkat terhubung ditemukan", "notFound": "Tidak ada perangkat Bluetooth yang terhubung ditemukan", @@ -180,22 +180,9 @@ const Map idID = { "conferenceAssistant": "Asisten Rapat Bisnis", "conferenceAssistantInfo": "Catatan rapat waktu nyata, terjemahan multibahasa", - "studyAssistant": "Asisten Belajar Bahasa", - "studyAssistantInfo": "Koreksi pengucapan, latihan berbicara waktu nyata", - "travelTranslator": "Asisten Terjemahan Perjalanan", - "travelTranslatorInfo": - "Terjemahan suara waktu nyata, pengenalan tempat wisata", - "beSearching": "Mencari:", - "noAssistantFound": "Tidak ada asisten AI yang cocok ditemukan", + "usageStats": "Statistik Penggunaan", + "usageStatsInfo": "Lihat statistik penggunaan aplikasi", "scene": "Skenario", - "youHaveSelected": "Anda telah memilih:", - "newFunction": "Fitur Baru", - "youChoseTry": "Anda memilih untuk mencoba", - "aiSinger": "Mode Penyanyi AI", - "aiSingerInfo": - "Nyanyikan lagu apa pun dengan suara penyanyi favorit Anda, bahkan lagu ciptaan Anda sendiri", - "popularFunction": "Skenario Fitur Populer", - "tryNow": "Coba Sekarang", // 商务会议助手页 "startMeeting": "Mulai Rapat", @@ -211,8 +198,12 @@ const Map idID = { 'meetingTranscript': 'Transkrip Rapat', // 设置页 - "systemSettings": "Pengaturan Sistem", - "premiumMember": "Anggota Premium", + "systemSettings": "Pengaturan Sistem", //系统设置 + "vipRecharge": "Pembayaran Premium", //会员充值 + "aiRecharge": "Anggota Premium", //AI充值 + "translationRecharge": "Pembayaran Terjemahan", //翻译充值 + "meetingAssistantRecharge": "Pembayaran Asisten Rapat", //会议助手充值 + "cloudSync": "Pembayaran Sinkronisasi awan", //云端同步 "joinDevice": "Perangkat terhubung:", "accountId": "ID Akun:", "systemSetting": "Pengaturan Sistem", @@ -281,7 +272,7 @@ const Map idID = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Hapus Riwayat Obrolan", "online": "Online", "inputMessage": "Masukkan pesan...", @@ -309,7 +300,7 @@ const Map idID = { "swipeUpForMore": "Geser ke atas untuk lebih banyak", // Agent controller - 'welcomeMessage': 'Halo! Saya **XiaoYan**, saya bisa membantu Anda:\n\n' + 'welcomeMessage': 'Halo! Saya **Lily**, saya bisa membantu Anda:\n\n' '• Menjawab pertanyaan dan memberikan informasi\n\n' '• Terjemahan suara waktu nyata\n\n' '• Merekomendasikan musik dan podcast\n\n' @@ -698,7 +689,7 @@ const Map idID = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Pemasangan Perangkat", - "connectYourDeepSoundHeadset": "Hubungkan Headset DeepSound Anda", + "connectYourDeepSoundHeadset": "Hubungkan Headset Voitrans Anda", "bind": "Ikat", "authorizeBind": "Otorisasi Pengikatan", "enterCode": "Silakan masukkan kode", @@ -752,7 +743,7 @@ const Map idID = { 'retryAuthorization': 'Coba Ulang Otorisasi', 'pleaseGrantNecessaryPermissions': 'Harap berikan izin yang diperlukan', 'checkFailedRetry': 'Pemeriksaan gagal, silakan coba lagi', - 'pleaseConnectDeepSoundDevice': 'Harap hubungkan perangkat DeepSound', + 'pleaseConnectDeepSoundDevice': 'Harap hubungkan perangkat Voitrans', 'verificationFailedRetry': 'Verifikasi gagal, silakan coba lagi', 'invalidDeviceAddress': 'Alamat perangkat tidak valid', 'bleDeviceAssociationFailed': 'Asosiasi perangkat BLE gagal', diff --git a/lib/core/translations/language/is_is.dart b/lib/core/translations/language/is_is.dart index f5bfbe00f..2e8e5a424 100644 --- a/lib/core/translations/language/is_is.dart +++ b/lib/core/translations/language/is_is.dart @@ -135,7 +135,8 @@ const Map isIS = { "initializing": "Frumstilli...", "bluetoothInitializationFailure": "Frumstilling Bluetooth-þjónustu mistókst", "checkingPermissions": "Athuga heimildir...", - "lackPermissions": "Skortir Bluetooth-heimildir", + "lackPermissions": + "Skortir Bluetooth-heimildir og lokatæknið er þó ekki tilgengilegt", "searchingDevice": "Leita að tækjum...", "findConnectedDevice": "Fann tengt tæki", "notFound": "Ekkert tengt Bluetooth-tæki fannst", @@ -180,21 +181,9 @@ const Map isIS = { "healthConsultantInfo": "Býður upp á fleiri gervigreindarspjallaðgerðir", "conferenceAssistant": "Aðstoðarmaður fyrir viðskiptafundi", "conferenceAssistantInfo": "Skráning fundargerða í rauntíma, fjöltyng þýðing", - "studyAssistant": "Aðstoðarmaður við tungumálanám", - "studyAssistantInfo": "Framburðarleiðrétting, rauntímaæfing í tali", - "travelTranslator": "Aðstoðarmaður við ferðaþýðingu", - "travelTranslatorInfo": "Rauntíma raddþýðing, kynning á ferðamannastöðum", - "beSearching": "Leita:", - "noAssistantFound": "Enginn samsvarandi gervigreindar-aðstoðarmaður fannst", + "usageStats": "Notendastatistika", + "usageStatsInfo": " skoða notendastatistika", "scene": "Atburðarás", - "youHaveSelected": "Þú valdir:", - "newFunction": "Nýr eiginleiki", - "youChoseTry": "Þú valdir að prófa", - "aiSinger": "Gervigreindarsöngvarahamur", - "aiSingerInfo": - "Syngdu hvaða lag sem er með rödd uppáhalds söngvarans þíns, jafnvel þínar eigin tónsmíðar", - "popularFunction": "Vinsælir eiginleikaatburðir", - "tryNow": "Prófa núna", // 商务会议助手页 "startMeeting": "Hefja fund", @@ -210,8 +199,12 @@ const Map isIS = { 'meetingTranscript': 'Fundarritun', // 设置页 - "systemSettings": "Kerfisstillingar", - "premiumMember": "Premium meðlimur", + "systemSettings": "Kerfisstillingar", //系统设置 + "vipRecharge": "Premium meðlimurinnsla", //会员充值 + "aiRecharge": "Premium meðlimur", //AI充值 + "translationRecharge": "Þýðingsponsoring", //翻译充值 + "meetingAssistantRecharge": "Fundarasistentsponsoring", //会议助手充值 + "cloudSync": "Synchronisering með skýjaraddþjónustu", //云端同步 "joinDevice": "Tengt tæki:", "accountId": "Auðkenni reiknings:", "systemSetting": "Kerfisstillingar", @@ -280,7 +273,7 @@ const Map isIS = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Hreinsa spjallsögu", "online": "Á netinu", "inputMessage": "Sláðu inn skilaboð...", @@ -307,7 +300,7 @@ const Map isIS = { "swipeUpForMore": "Strjúktu upp til að sjá meira", // Agent controller - 'welcomeMessage': 'Halló! Ég er **XiaoYan**, ég get hjálpað þér með:\n\n' + 'welcomeMessage': 'Halló! Ég er **Lily**, ég get hjálpað þér með:\n\n' '• Að svara spurningum og veita upplýsingar\n\n' '• Rauntíma raddþýðingu\n\n' '• Að mæla með tónlist og hlaðvörpum\n\n' @@ -698,7 +691,7 @@ const Map isIS = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Pörun tækis", - "connectYourDeepSoundHeadset": "Tengdu DeepSound heyrnartólin þín", + "connectYourDeepSoundHeadset": "Tengdu Voitrans heyrnartólin þín", "bind": "Binda", "authorizeBind": "Heimila bindingu", "enterCode": "Vinsamlegast sláðu inn kóðann", diff --git a/lib/core/translations/language/it_it.dart b/lib/core/translations/language/it_it.dart index 07a2e32f8..fece149d3 100644 --- a/lib/core/translations/language/it_it.dart +++ b/lib/core/translations/language/it_it.dart @@ -137,7 +137,8 @@ const Map itIT = { "bluetoothInitializationFailure": "Inizializzazione del servizio Bluetooth fallita", "checkingPermissions": "Verifica autorizzazioni...", - "lackPermissions": "Mancano le autorizzazioni Bluetooth", + "lackPermissions": + "Mancano le autorizzazioni Bluetooth e l'accesso alla posizione è necessario", "searchingDevice": "Ricerca dispositivi...", "findConnectedDevice": "Dispositivo connesso trovato", "notFound": "Nessun dispositivo Bluetooth connesso trovato", @@ -185,23 +186,9 @@ const Map itIT = { "conferenceAssistant": "Assistente per riunioni di lavoro", "conferenceAssistantInfo": "Registrazione di verbali in tempo reale, traduzione multilingue", - "studyAssistant": "Assistente per l'apprendimento delle lingue", - "studyAssistantInfo": - "Correzione della pronuncia, pratica della conversazione in tempo reale", - "travelTranslator": "Assistente di traduzione per viaggi", - "travelTranslatorInfo": - "Traduzione vocale in tempo reale, introduzione alle attrazioni turistiche", - "beSearching": "Ricerca in corso:", - "noAssistantFound": "Nessun assistente AI corrispondente trovato", + "usageStats": "Statistiche di utilizzo", + "usageStatsInfo": "Visualizza le statistiche di utilizzo dell'applicazione", "scene": "Scenario", - "youHaveSelected": "Hai selezionato:", - "newFunction": "Nuova funzione", - "youChoseTry": "Hai scelto di provare", - "aiSinger": "Modalità cantante AI", - "aiSingerInfo": - "Canta qualsiasi canzone con la voce del tuo cantante preferito, anche le tue composizioni", - "popularFunction": "Scenari di funzioni popolari", - "tryNow": "Prova ora", // 商务会议助手页 "startMeeting": "Inizia riunione", @@ -217,8 +204,12 @@ const Map itIT = { 'meetingTranscript': 'Trascrizione riunione', // 设置页 - "systemSettings": "Impostazioni di sistema", - "premiumMember": "Membro Premium", + "systemSettings": "Impostazioni di sistema", //系统设置 + "vipRecharge": "Ricarica membro Premium", //会员充值 + "aiRecharge": "Membro Premium", //AI充值 + "translationRecharge": "Recharge traduzione", //翻译充值 + "meetingAssistantRecharge": "Recharge assistente riunione", //会议助手充值 + "cloudSync": "Recharge sincronizzazione cloud", //云端同步 "joinDevice": "Dispositivo connesso:", "accountId": "ID account:", "systemSetting": "Impostazioni di sistema", @@ -291,7 +282,7 @@ const Map itIT = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Cancella cronologia chat", "online": "Online", "inputMessage": "Inserisci messaggio...", @@ -319,7 +310,7 @@ const Map itIT = { "swipeUpForMore": "Scorri verso l'alto per vedere di più", // Agent controller - 'welcomeMessage': 'Ciao! Sono **XiaoYan**, posso aiutarti a:\n\n' + 'welcomeMessage': 'Ciao! Sono **Lily**, posso aiutarti a:\n\n' '• Rispondere a domande e fornire informazioni\n\n' '• Traduzione vocale in tempo reale\n\n' '• Consigliare musica e podcast\n\n' @@ -717,7 +708,7 @@ const Map itIT = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Associazione dispositivo", - "connectYourDeepSoundHeadset": "Connetti le tue cuffie DeepSound", + "connectYourDeepSoundHeadset": "Connetti le tue cuffie Voitrans", "bind": "Associa", "authorizeBind": "Autorizza associazione", "enterCode": "Inserisci il codice", @@ -774,7 +765,7 @@ const Map itIT = { 'retryAuthorization': 'Riprova autorizzazione', 'pleaseGrantNecessaryPermissions': 'Concedi le autorizzazioni necessarie', 'checkFailedRetry': 'Controllo fallito, riprova', - 'pleaseConnectDeepSoundDevice': 'Connetti un dispositivo DeepSound', + 'pleaseConnectDeepSoundDevice': 'Connetti un dispositivo Voitrans', 'verificationFailedRetry': 'Verifica fallita, riprova', 'invalidDeviceAddress': 'Indirizzo del dispositivo non valido', 'bleDeviceAssociationFailed': 'Associazione del dispositivo BLE fallita', @@ -869,4 +860,98 @@ const Map itIT = { "乌尔都语": "Urdu", "波斯语": "Persiano", "斯瓦希里语": "Swahili", + // Guide overlay keys (placeholders in English) + "sayLilyHint": "Say \"Lily Lily\" to the mic", + "imHere": "I’m here~", + "guideWelcomeWakeAI": "Welcome to Voitrans! First, wake your AI “Lily”.", + "aiReplyCompleted": "Lily has finished the reply!", + "aiThinking": "Lily is thinking…", + "guideAwakenSuccess": "Great! Lily is awake—ask it to help.", + "confirmInput": "Confirm Input", + "guideClickSendPrefix": "Tap send, ", + "guideWeatherStart": "Tell Lily: “What’s the weather today?”", + "guideWeatherInput": "What’s the weather today?", + "guideWeatherSend": "Lily will report the weather in real time", + "guideWeatherEnd": "Lily can report weather live—try other commands", + "guideMusicStart": "Tell Lily: “Play some rock music”", + "guideMusicInput": "Play some rock music", + "guideMusicSend": "Lily will find music for you", + "guideMusicEnd": "Lily found music—try other commands", + "guideNavStart": "Tell Lily: “Navigate to Shenzhen Airport”", + "guideNavInput": "Navigate to Shenzhen Airport", + "guideNavSend": "Lily will plan the route", + "guideNavEnd": "Navigation set—try other commands", + "guideMeetingStart": + "Tell Lily: “Create a meeting reminder for 3pm tomorrow”", + "guideMeetingInput": "Create a meeting reminder for 3pm tomorrow", + "guideMeetingSend": "Lily will create a meeting reminder", + "guideMeetingEnd": "Reminder created—try other commands", + "guidePhoneStart": "Tell Lily: “Call Mom”", + "guidePhoneInput": "Call Mom", + "guidePhoneSend": "Lily will make the call", + "guidePhoneEnd": "Call placed—try other commands", + "guideSmsStart": "Tell Lily: “Send Dad a text: I’ll be home for dinner”", + "guideSmsInput": "Text Dad: I’ll be home for dinner", + "guideSmsSend": "Lily will send the message", + "guideSmsEnd": "Message sent—voice control is convenient", + // Goods pages common footer (placeholders) + 'appNameProvider': 'App: VoiTrans · Provided by: VoiTrans', + 'copyrightVoitrans': '© 2025 Voitrans Technologies', + + // Meeting goods page (placeholders) + 'meetingHeaderDesc1': + 'Audio recording, transcription, keyword search, and auto summary.', + 'meetingHeaderDesc2': + 'Suitable for business meetings, class notes, and interview minutes.', + 'meetingAutoRecording': 'Auto recording and segmentation', + 'meetingSpeakerTagging': 'Speaker labeling', + 'meetingLongAudioTranscription': 'Long audio transcription', + 'meetingKeyPointSummary': 'Key point extraction', + 'meetingTopicNameSearch': 'Search by topic/name', + 'meetingTimelineJump': 'Timeline jump', + 'meetingExportSummary': 'Export minutes', + 'meetingMultiPlatformShare': 'Share across platforms', + 'meetingDisclaimer': + 'Use in quiet environments; performance depends on microphone and network.', + + // Translation goods page (placeholders) + 'transHeaderDesc1': + 'Interpretazione simultanea e traduzione di testo in tempo reale, con voce a bassa latenza.', + 'transHeaderDesc2': 'Adatto a viaggi, affari, discorsi e insegnamento.', + 'translateZhEn': 'Traduzione Cinese-Inglese', + 'lowLatencyVoiceBroadcast': 'Trasmissione vocale a bassa latenza', + 'multiLangTextTranslation': 'Traduzione di testo multilingue', + 'termOptimizationContextPreserve': + 'Ottimizzazione della terminologia e mantenimento del contesto', + 'lexiconTourBusinessLife': 'Lessici per viaggio/affari/vita quotidiana', + 'personalVocabularyMemory': 'Memoria del vocabolario personale', + 'conversationSubtitleLargeText': + 'Sottotitoli conversazione / modalità testo grande', + 'colorHighContrast': 'Elevato contrasto dei colori', + 'transDisclaimer': + 'Le prestazioni della traduzione dipendono dal microfono, dalla latenza di rete e dal rumore ambientale.', + + 'trans_001_name': 'Traduzione Base 100', + 'trans_002_name': 'Traduzione Base 500', + 'trans_003_name': 'Traduzione Base 1000', + + 'trans_001_desc': 'Include 100 traduzioni (voce/testo).', + 'trans_002_desc': 'Include 500 traduzioni (voce/testo).', + 'trans_003_desc': 'Include 1000 traduzioni (voce/testo).', + + // VIP goods page (placeholders) + 'benefitsOverview': 'Panoramica dei vantaggi', + 'higherQuota': 'Quota di utilizzo più alta', + 'lowerUnitPrice': 'Prezzo unitario più basso', + 'priorityService': 'Servizio prioritario', + 'exclusiveSupportChannel': 'Canale di supporto dedicato', + 'fastResponse': 'Risposta rapida ai problemi', + 'eventsAndDiscounts': 'Eventi e sconti', + 'memberExclusiveEvents': 'Eventi esclusivi per i membri', + 'longTermDiscountPlan': 'Piano di sconti a lungo termine', + 'generalPrivileges': 'Privilegi generali', + 'earlyAccessNewFeatures': 'Accesso anticipato alle nuove funzionalità', + 'crossDeviceSync': 'Sincronizzazione tra dispositivi', + 'vipDisclaimer': + 'I benefici effettivi dipendono dai dettagli del pacchetto e possono cambiare con le versioni.', }; diff --git a/lib/core/translations/language/ja_jp.dart b/lib/core/translations/language/ja_jp.dart index 360a0207c..4e9bffa4f 100644 --- a/lib/core/translations/language/ja_jp.dart +++ b/lib/core/translations/language/ja_jp.dart @@ -132,7 +132,7 @@ const Map jaJP = { "initializing": "初期化中...", "bluetoothInitializationFailure": "Bluetoothサービスの初期化に失敗", "checkingPermissions": "権限を確認中...", - "lackPermissions": "Bluetooth権限が必要", + "lackPermissions": "Bluetooth権限が必要です。位置情報のアクセスも必要です", "searchingDevice": "デバイスを検索中...", "findConnectedDevice": "接続されたデバイスが見つかりました", "notFound": "接続されたBluetoothデバイスが見つかりません", @@ -176,20 +176,9 @@ const Map jaJP = { "healthConsultantInfo": "より多くのAIチャット機能", "conferenceAssistant": "ビジネス会議アシスタント", "conferenceAssistantInfo": "リアルタイム議事録記録と多言語翻訳", - "studyAssistant": "語学学習アシスタント", - "studyAssistantInfo": "発音矯正とスピーキング練習", - "travelTranslator": "旅行翻訳者", - "travelTranslatorInfo": "リアルタイム音声翻訳と観光地ガイド", - "beSearching": "検索中:", - "noAssistantFound": "対応するAIアシスタントが見つかりません", + "usageStats": "使用統計", + "usageStatsInfo": "使用統計を確認", "scene": "シナリオ", - "youHaveSelected": "選択しました:", - "newFunction": "新機能", - "youChoseTry": "試すことを選択しました", - "aiSinger": "AIシンガーモード", - "aiSingerInfo": "お気に入りのアーティストの声で任意の曲を歌う、自分の創作も可能", - "popularFunction": "人気機能", - "tryNow": "今すぐ試す", // 商务会议助手页 "startMeeting": "会議を開始", @@ -202,11 +191,15 @@ const Map jaJP = { 'meeting': '会議', 'recordingDetails': '録音詳細', 'aiSummary': 'AI要約', - 'meetingTranscript': '会議記録', + 'meetingTranscript': '会議記録', //会议记录 // 设置页 - "systemSettings": "システム設定", - "premiumMember": "プレミアムメンバー", + "systemSettings": "システム設定", //系统设置 + "vipRecharge": "プレミアムメンバー料金", //会员充值 + "aiRecharge": "プレミアムメンバー", //AI充值 + "translationRecharge": "翻訳料金", //翻译充值 + "meetingAssistantRecharge": "会議アシスタント料金", //会议助手充值 + "cloudSync": "クラウド同期料金", //云端同步 "joinDevice": "接続デバイス:", "accountId": "アカウントID:", "systemSetting": "システム設定", @@ -271,7 +264,7 @@ const Map jaJP = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "チャット履歴をクリア", "online": "オンライン", "inputMessage": "メッセージを入力...", @@ -298,7 +291,7 @@ const Map jaJP = { "swipeUpForMore": "上にスワイプしてさらに表示", // Agent controller - 'welcomeMessage': 'こんにちは!私は**XiaoYan**です。以下のことでお手伝いできます:\n\n' + 'welcomeMessage': 'こんにちは!私は**Lily**です。以下のことでお手伝いできます:\n\n' '• 質問への回答と情報提供\n\n' '• リアルタイム音声翻訳\n\n' '• 音楽とポッドキャストの推薦\n\n' @@ -797,4 +790,38 @@ const Map jaJP = { "乌尔都语": "ウルドゥー語", "波斯语": "ペルシャ語", "斯瓦希里语": "スワヒリ語", + // Guide overlay keys (placeholders in English) + "sayLilyHint": "Say \"Lily Lily\" to the mic", + "imHere": "I’m here~", + "guideWelcomeWakeAI": "Welcome to Voitrans! First, wake your AI “Lily”.", + "aiReplyCompleted": "Lily has finished the reply!", + "aiThinking": "Lily is thinking…", + "guideAwakenSuccess": "Great! Lily is awake—ask it to help.", + "confirmInput": "Confirm Input", + "guideClickSendPrefix": "Tap send, ", + "guideWeatherStart": "Tell Lily: “What’s the weather today?”", + "guideWeatherInput": "What’s the weather today?", + "guideWeatherSend": "Lily will report the weather in real time", + "guideWeatherEnd": "Lily can report weather live—try other commands", + "guideMusicStart": "Tell Lily: “Play some rock music”", + "guideMusicInput": "Play some rock music", + "guideMusicSend": "Lily will find music for you", + "guideMusicEnd": "Lily found music—try other commands", + "guideNavStart": "Tell Lily: “Navigate to Shenzhen Airport”", + "guideNavInput": "Navigate to Shenzhen Airport", + "guideNavSend": "Lily will plan the route", + "guideNavEnd": "Navigation set—try other commands", + "guideMeetingStart": + "Tell Lily: “Create a meeting reminder for 3pm tomorrow”", + "guideMeetingInput": "Create a meeting reminder for 3pm tomorrow", + "guideMeetingSend": "Lily will create a meeting reminder", + "guideMeetingEnd": "Reminder created—try other commands", + "guidePhoneStart": "Tell Lily: “Call Mom”", + "guidePhoneInput": "Call Mom", + "guidePhoneSend": "Lily will make the call", + "guidePhoneEnd": "Call placed—try other commands", + "guideSmsStart": "Tell Lily: “Send Dad a text: I’ll be home for dinner”", + "guideSmsInput": "Text Dad: I’ll be home for dinner", + "guideSmsSend": "Lily will send the message", + "guideSmsEnd": "Message sent—voice control is convenient", }; diff --git a/lib/core/translations/language/ko_kr.dart b/lib/core/translations/language/ko_kr.dart index 2b1c2122e..5067aae43 100644 --- a/lib/core/translations/language/ko_kr.dart +++ b/lib/core/translations/language/ko_kr.dart @@ -130,7 +130,7 @@ const Map koKR = { "initializing": "초기화 중...", "bluetoothInitializationFailure": "블루투스 서비스 초기화 실패", "checkingPermissions": "권한 확인 중...", - "lackPermissions": "블루투스 권한이 없습니다", + "lackPermissions": "블루투스 권한이 없습니다. 위치 정보 접근도 필요합니다", "searchingDevice": "기기 검색 중...", "findConnectedDevice": "연결된 기기를 찾았습니다", "notFound": "연결된 블루투스 기기를 찾을 수 없습니다", @@ -174,20 +174,9 @@ const Map koKR = { "healthConsultantInfo": "더 많은 AI 채팅 기능 제공", "conferenceAssistant": "비즈니스 회의 어시스턴트", "conferenceAssistantInfo": "실시간 회의록 작성, 다국어 번역", - "studyAssistant": "언어 학습 어시스턴트", - "studyAssistantInfo": "발음 교정, 실시간 회화 연습", - "travelTranslator": "여행 번역 어시스턴트", - "travelTranslatorInfo": "실시간 음성 번역, 관광지 소개", - "beSearching": "검색 중:", - "noAssistantFound": "해당하는 AI 어시스턴트를 찾을 수 없습니다", + "usageStats": "사용 통계", + "usageStatsInfo": "사용 통계를 확인하세요", "scene": "상황", - "youHaveSelected": "선택하셨습니다:", - "newFunction": "새로운 기능", - "youChoseTry": "시도하기로 선택하셨습니다", - "aiSinger": "AI 가수 모드", - "aiSingerInfo": "좋아하는 가수의 목소리로 어떤 노래든 부르세요, 심지어 직접 만든 노래까지", - "popularFunction": "인기 기능 상황", - "tryNow": "지금 시도하기", // 商务会议助手页 "startMeeting": "회의 시작", @@ -203,8 +192,12 @@ const Map koKR = { 'meetingTranscript': '회의 원문', // 设置页 - "systemSettings": "시스템 설정", - "premiumMember": "프리미엄 회원", + "systemSettings": "시스템 설정", //系统设置 + "vipRecharge": "프리미엄 회원료", //会员充值 + "aiRecharge": "프리미엄 회원", //AI充值 + "translationRecharge": "번역료", //翻译充值 + "meetingAssistantRecharge": "회의 어시스턴트료", //会议助手充值 + "cloudSync": "클라우드 동기화료", //云端同步 "joinDevice": "연결된 기기:", "accountId": "계정 ID:", "systemSetting": "시스템 설정", @@ -269,7 +262,7 @@ const Map koKR = { //Agent 模块 // agent View - "XiaoYan": "샤오옌", + "Lily": "Lily", "clearChatHistory": "채팅 기록 지우기", "online": "온라인", "inputMessage": "메시지 입력...", @@ -649,7 +642,7 @@ const Map koKR = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "기기 페어링", - "connectYourDeepSoundHeadset": "DeepSound 헤드셋 연결", + "connectYourDeepSoundHeadset": "Voitrans 헤드셋 연결", "bind": "연결", "authorizeBind": "연결 승인", "enterCode": "코드를 입력하세요", @@ -696,7 +689,7 @@ const Map koKR = { 'retryAuthorization': '인증 재시도', 'pleaseGrantNecessaryPermissions': '필요한 권한을 부여해주세요', 'checkFailedRetry': '확인 실패, 다시 시도하세요', - 'pleaseConnectDeepSoundDevice': 'DeepSound 기기를 연결해주세요', + 'pleaseConnectDeepSoundDevice': 'Voitrans 기기를 연결해주세요', 'verificationFailedRetry': '인증 실패, 다시 시도하세요', 'invalidDeviceAddress': '잘못된 기기 주소', 'bleDeviceAssociationFailed': 'BLE 기기 연결 실패', @@ -784,4 +777,93 @@ const Map koKR = { "乌尔都语": "우르두어", "波斯语": "페르시아어", "斯瓦希里语": "스와힐리어", + // Guide overlay keys (placeholders in English) + "sayLilyHint": "Say \"Lily Lily\" to the mic", + "imHere": "I’m here~", + "guideWelcomeWakeAI": "Welcome to Voitrans! First, wake your AI “Lily”.", + "aiReplyCompleted": "Lily has finished the reply!", + "aiThinking": "Lily is thinking…", + "guideAwakenSuccess": "Great! Lily is awake—ask it to help.", + "confirmInput": "Confirm Input", + "guideClickSendPrefix": "Tap send, ", + "guideWeatherStart": "Tell Lily: “What’s the weather today?”", + "guideWeatherInput": "What’s the weather today?", + "guideWeatherSend": "Lily will report the weather in real time", + "guideWeatherEnd": "Lily can report weather live—try other commands", + "guideMusicStart": "Tell Lily: “Play some rock music”", + "guideMusicInput": "Play some rock music", + "guideMusicSend": "Lily will find music for you", + "guideMusicEnd": "Lily found music—try other commands", + "guideNavStart": "Tell Lily: “Navigate to Shenzhen Airport”", + "guideNavInput": "Navigate to Shenzhen Airport", + "guideNavSend": "Lily will plan the route", + "guideNavEnd": "Navigation set—try other commands", + "guideMeetingStart": + "Tell Lily: “Create a meeting reminder for 3pm tomorrow”", + "guideMeetingInput": "Create a meeting reminder for 3pm tomorrow", + "guideMeetingSend": "Lily will create a meeting reminder", + "guideMeetingEnd": "Reminder created—try other commands", + "guidePhoneStart": "Tell Lily: “Call Mom”", + "guidePhoneInput": "Call Mom", + "guidePhoneSend": "Lily will make the call", + "guidePhoneEnd": "Call placed—try other commands", + "guideSmsStart": "Tell Lily: “Send Dad a text: I’ll be home for dinner”", + "guideSmsInput": "Text Dad: I’ll be home for dinner", + "guideSmsSend": "Lily will send the message", + "guideSmsEnd": "Message sent—voice control is convenient", + // Goods pages common footer (placeholders) + 'appNameProvider': 'App: VoiTrans · Provided by: VoiTrans', + 'copyrightVoitrans': '© 2025 Voitrans Technologies', + + // Meeting goods page (placeholders) + 'meetingHeaderDesc1': + 'Audio recording, transcription, keyword search, and auto summary.', + 'meetingHeaderDesc2': + 'Suitable for business meetings, class notes, and interview minutes.', + 'meetingAutoRecording': 'Auto recording and segmentation', + 'meetingSpeakerTagging': 'Speaker labeling', + 'meetingLongAudioTranscription': 'Long audio transcription', + 'meetingKeyPointSummary': 'Key point extraction', + 'meetingTopicNameSearch': 'Search by topic/name', + 'meetingTimelineJump': 'Timeline jump', + 'meetingExportSummary': 'Export minutes', + 'meetingMultiPlatformShare': 'Share across platforms', + 'meetingDisclaimer': + 'Use in quiet environments; performance depends on microphone and network.', + + // Translation goods page (placeholders) + 'transHeaderDesc1': '실시간 동시통역과 텍스트 번역, 저지연 음성 방송.', + 'transHeaderDesc2': '여행, 비즈니스, 연설, 교육에 적합합니다.', + 'translateZhEn': '중-영 번역', + 'lowLatencyVoiceBroadcast': '저지연 음성 방송', + 'multiLangTextTranslation': '다국어 텍스트 번역', + 'termOptimizationContextPreserve': '용어 최적화 및 문맥 유지', + 'lexiconTourBusinessLife': '여행/비즈니스/일상 용어집', + 'personalVocabularyMemory': '개인 어휘 메모리', + 'conversationSubtitleLargeText': '대화 자막 / 큰 글자 모드', + 'colorHighContrast': '높은 색 대비', + 'transDisclaimer': '번역 품질은 마이크, 네트워크 지연 및 주변 소음에 따라 달라집니다.', + + 'trans_001_name': '번역 기본 100', + 'trans_002_name': '번역 기본 500', + 'trans_003_name': '번역 기본 1000', + + 'trans_001_desc': '번역 100회 포함(음성/텍스트).', + 'trans_002_desc': '번역 500회 포함(음성/텍스트).', + 'trans_003_desc': '번역 1000회 포함(음성/텍스트).', + + // VIP goods page (placeholders) + 'benefitsOverview': '혜택 개요', + 'higherQuota': '더 높은 사용 한도', + 'lowerUnitPrice': '더 낮은 단가', + 'priorityService': '우선 서비스', + 'exclusiveSupportChannel': '전용 지원 채널', + 'fastResponse': '문제 신속 대응', + 'eventsAndDiscounts': '이벤트 및 할인', + 'memberExclusiveEvents': '회원 전용 이벤트', + 'longTermDiscountPlan': '장기 할인 계획', + 'generalPrivileges': '일반 특권', + 'earlyAccessNewFeatures': '신기능 사전 이용', + 'crossDeviceSync': '디바이스 간 동기화', + 'vipDisclaimer': '실제 혜택은 패키지 세부사항에 따라 달라지며 버전에 따라 변경될 수 있습니다.', }; diff --git a/lib/core/translations/language/lt_lt.dart b/lib/core/translations/language/lt_lt.dart index 7df67f0b0..9bcdaca54 100644 --- a/lib/core/translations/language/lt_lt.dart +++ b/lib/core/translations/language/lt_lt.dart @@ -137,7 +137,8 @@ const Map ltLT = { "initializing": "Inicijuojama...", "bluetoothInitializationFailure": "Bluetooth paslaugos inicijavimas nepavyko", "checkingPermissions": "Tikrinami leidimai...", - "lackPermissions": "Trūksta Bluetooth leidimų", + "lackPermissions": + "Trūksta Bluetooth leidimų ir yra reikalinga lokacijos prieiga", "searchingDevice": "Ieškoma įrenginių...", "findConnectedDevice": "Rastas prijungtas įrenginys", "notFound": "Nerasta prijungto Bluetooth įrenginio", @@ -183,22 +184,9 @@ const Map ltLT = { "conferenceAssistant": "Verslo susitikimų asistentas", "conferenceAssistantInfo": "Susitikimų protokolo įrašymas realiuoju laiku, daugiakalbis vertimas", - "studyAssistant": "Kalbos mokymosi asistentas", - "studyAssistantInfo": "Tarimo korekcija, kalbėjimo praktika realiuoju laiku", - "travelTranslator": "Kelionių vertimo asistentas", - "travelTranslatorInfo": - "Balso vertimas realiuoju laiku, turistinių objektų pristatymas", - "beSearching": "Ieškoma:", - "noAssistantFound": "Nerasta atitinkamo AI asistento", + "usageStats": "Naudojimo statistika", + "usageStatsInfo": "Peržiūrėkite jūsų naudojimo statistiką", "scene": "Scenarijus", - "youHaveSelected": "Pasirinkote:", - "newFunction": "Nauja funkcija", - "youChoseTry": "Pasirinkote išbandyti", - "aiSinger": "AI dainininko režimas", - "aiSingerInfo": - "Dainuokite bet kurią dainą mėgstamo dainininko balsu, net savo kompozicijas", - "popularFunction": "Populiarūs funkcijų scenarijai", - "tryNow": "Išbandyti dabar", // 商务会议助手页 "startMeeting": "Pradėti susitikimą", @@ -214,8 +202,12 @@ const Map ltLT = { 'meetingTranscript': 'Susitikimo stenograma', // 设置页 - "systemSettings": "Sistemos nustatymai", - "premiumMember": "Premium narys", + "systemSettings": "Sistemos nustatymai", //系统设置 + "vipRecharge": "Premium naryso užsipirkimas", //会员充值 + "aiRecharge": "Premium narys", //AI充值 + "translationRecharge": "Vertimo užsipirkimas", //翻译充值 + "meetingAssistantRecharge": "Susitikimų asistento užsipirkimas", //会议助手充值 + "cloudSync": "Synchronizacijos užsipirkimas", //云端同步 "joinDevice": "Prijungtas įrenginys:", "accountId": "Paskyros ID:", "systemSetting": "Sistemos nustatymas", @@ -285,7 +277,7 @@ const Map ltLT = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Išvalyti pokalbių istoriją", "online": "Prisijungęs", "inputMessage": "Įveskite žinutę...", @@ -313,7 +305,7 @@ const Map ltLT = { "swipeUpForMore": "Braukite aukštyn, kad pamatytumėte daugiau", // Agent controller - 'welcomeMessage': 'Sveiki! Aš esu **XiaoYan**, galiu jums padėti:\n\n' + 'welcomeMessage': 'Sveiki! Aš esu **Lily**, galiu jums padėti:\n\n' '• Atsakyti į klausimus ir pateikti informaciją\n\n' '• Balso vertimas realiuoju laiku\n\n' '• Rekomenduoti muziką ir tinklalaides\n\n' @@ -703,7 +695,7 @@ const Map ltLT = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Įrenginio susiejimas", - "connectYourDeepSoundHeadset": "Prijunkite savo DeepSound ausines", + "connectYourDeepSoundHeadset": "Prijunkite savo Voitrans ausines", "bind": "Susieti", "authorizeBind": "Leisti susiejimą", "enterCode": "Įveskite kodą", @@ -759,7 +751,7 @@ const Map ltLT = { 'retryAuthorization': 'Bandyti autorizaciją dar kartą', 'pleaseGrantNecessaryPermissions': 'Suteikite reikiamus leidimus', 'checkFailedRetry': 'Patikrinimas nepavyko, bandykite dar kartą', - 'pleaseConnectDeepSoundDevice': 'Prijunkite DeepSound įrenginį', + 'pleaseConnectDeepSoundDevice': 'Prijunkite Voitrans įrenginį', 'verificationFailedRetry': 'Patvirtinimas nepavyko, bandykite dar kartą', 'invalidDeviceAddress': 'Neteisingas įrenginio adresas', 'bleDeviceAssociationFailed': 'BLE įrenginio asociacija nepavyko', diff --git a/lib/core/translations/language/lv_lv.dart b/lib/core/translations/language/lv_lv.dart index 5b9a89a67..97f316618 100644 --- a/lib/core/translations/language/lv_lv.dart +++ b/lib/core/translations/language/lv_lv.dart @@ -137,7 +137,8 @@ const Map lvLV = { "bluetoothInitializationFailure": "Bluetooth pakalpojuma inicializācija neizdevās", "checkingPermissions": "Pārbauda atļaujas...", - "lackPermissions": "Trūkst Bluetooth atļauju", + "lackPermissions": + "Trūkst Bluetooth atļauju un ir nepieciešams piekļūt lokācijai", "searchingDevice": "Meklē ierīces...", "findConnectedDevice": "Atrasta savienota ierīce", "notFound": "Neviena savienota Bluetooth ierīce nav atrasta", @@ -182,22 +183,9 @@ const Map lvLV = { "conferenceAssistant": "Biznesa sanāksmju palīgs", "conferenceAssistantInfo": "Reāllaika protokolu ierakstīšana, daudzvalodu tulkošana", - "studyAssistant": "Valodu apguves palīgs", - "studyAssistantInfo": "Izrunas korekcija, reāllaika runas prakse", - "travelTranslator": "Ceļojumu tulkošanas palīgs", - "travelTranslatorInfo": - "Reāllaika balss tulkošana, tūrisma objektu iepazīstināšana", - "beSearching": "Meklē:", - "noAssistantFound": "Neviens atbilstošs AI palīgs nav atrasts", + "usageStats": "Pārskatījumu statistika", + "usageStatsInfo": "Pārskatiet jūsu lietotāja pārskatījumu statistiku", "scene": "Scenārijs", - "youHaveSelected": "Jūs izvēlējāties:", - "newFunction": "Jauna funkcija", - "youChoseTry": "Jūs izvēlējāties mēģināt", - "aiSinger": "AI dziedātāja režīms", - "aiSingerInfo": - "Dziediet jebkuru dziesmu ar sava iecienītākā dziedātāja balsi, pat savas kompozīcijas", - "popularFunction": "Populāri funkciju scenāriji", - "tryNow": "Izmēģināt tagad", // 商务会议助手页 "startMeeting": "Sākt sapulci", @@ -213,8 +201,12 @@ const Map lvLV = { 'meetingTranscript': 'Sapulces atšifrējums', // 设置页 - "systemSettings": "Sistēmas iestatījumi", - "premiumMember": "Premium biedrs", + "systemSettings": "Sistēmas iestatījumi", //系统设置 + "vipRecharge": "Premium biedra pārrakstīšana", //会员充值 + "aiRecharge": "Premium biedrs", //AI充值 + "translationRecharge": "Tulkošanas pārrakstīšana", //翻译充值 + "meetingAssistantRecharge": "Sapulces palīgs pārrakstīšana", //会议助手充值 + "cloudSync": "Tīkla sincronizācijas pārrakstīšana", //云端同步 "joinDevice": "Savienotā ierīce:", "accountId": "Konta ID:", "systemSetting": "Sistēmas iestatījumi", @@ -284,7 +276,7 @@ const Map lvLV = { //Agent 模块 // agent View - "XiaoYan": "SjaoJaņ", + "Lily": "Lily", "clearChatHistory": "Notīrīt tērzēšanas vēsturi", "online": "Tiešsaistē", "inputMessage": "Ievadiet ziņu...", @@ -705,7 +697,7 @@ const Map lvLV = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Ierīces savienošana pārī", - "connectYourDeepSoundHeadset": "Savienojiet savas DeepSound austiņas", + "connectYourDeepSoundHeadset": "Savienojiet savas Voitrans austiņas", "bind": "Saistīt", "authorizeBind": "Autorizēt saistīšanu", "enterCode": "Lūdzu, ievadiet kodu", @@ -760,7 +752,7 @@ const Map lvLV = { 'retryAuthorization': 'Mēģināt autorizāciju vēlreiz', 'pleaseGrantNecessaryPermissions': 'Lūdzu, piešķiriet nepieciešamās atļaujas', 'checkFailedRetry': 'Pārbaude neizdevās, lūdzu, mēģiniet vēlreiz', - 'pleaseConnectDeepSoundDevice': 'Lūdzu, savienojiet DeepSound ierīci', + 'pleaseConnectDeepSoundDevice': 'Lūdzu, savienojiet Voitrans ierīci', 'verificationFailedRetry': 'Verifikācija neizdevās, lūdzu, mēģiniet vēlreiz', 'invalidDeviceAddress': 'Nederīga ierīces adrese', 'bleDeviceAssociationFailed': 'BLE ierīces asociācija neizdevās', diff --git a/lib/core/translations/language/ms_my.dart b/lib/core/translations/language/ms_my.dart index 28dea6f4c..e1b998b45 100644 --- a/lib/core/translations/language/ms_my.dart +++ b/lib/core/translations/language/ms_my.dart @@ -136,7 +136,8 @@ const Map msMY = { "initializing": "Memulakan...", "bluetoothInitializationFailure": "Inisialisasi perkhidmatan Bluetooth gagal", "checkingPermissions": "Menyemak kebenaran...", - "lackPermissions": "Kebenaran Bluetooth tiada", + "lackPermissions": + "Kebenaran Bluetooth tiada dan perlu mengaktifkan lokasi peranti", "searchingDevice": "Mencari peranti...", "findConnectedDevice": "Peranti yang disambungkan ditemui", "notFound": "Tiada peranti Bluetooth yang disambungkan ditemui", @@ -181,22 +182,9 @@ const Map msMY = { "conferenceAssistant": "Pembantu Mesyuarat Perniagaan", "conferenceAssistantInfo": "Minit mesyuarat masa nyata, terjemahan pelbagai bahasa", - "studyAssistant": "Pembantu Pembelajaran Bahasa", - "studyAssistantInfo": "Pembetulan sebutan, latihan pertuturan masa nyata", - "travelTranslator": "Pembantu Terjemahan Perjalanan", - "travelTranslatorInfo": - "Terjemahan suara masa nyata, pengenalan tempat pelancongan", - "beSearching": "Mencari:", - "noAssistantFound": "Tiada pembantu AI yang sepadan ditemui", + "usageStats": "Statistik Penggunaan", + "usageStatsInfo": "Lihat statistik penggunaan aplikasi", "scene": "Senario", - "youHaveSelected": "Anda telah memilih:", - "newFunction": "Fungsi Baharu", - "youChoseTry": "Anda memilih untuk mencuba", - "aiSinger": "Mod Penyanyi AI", - "aiSingerInfo": - "Nyanyikan sebarang lagu dengan suara penyanyi kegemaran anda, malah lagu ciptaan anda sendiri", - "popularFunction": "Senario Fungsi Popular", - "tryNow": "Cuba Sekarang", // 商务会议助手页 "startMeeting": "Mula Mesyuarat", @@ -212,8 +200,12 @@ const Map msMY = { 'meetingTranscript': 'Transkrip Mesyuarat', // 设置页 - "systemSettings": "Tetapan Sistem", - "premiumMember": "Ahli Premium", + "systemSettings": "Tetapan Sistem", //系统设置 + "vipRecharge": "Pembayaran Ahli Premium", //会员充值 + "aiRecharge": "Ahli Premium", //AI充值 + "translationRecharge": "Pembayaran Penterjemahan", //翻译充值 + "meetingAssistantRecharge": "Pembayaran Pembantu Mesyuarat", //会议助手充值 + "cloudSync": "Pembayaran Sinkronisasi awan", //云端同步 "joinDevice": "Peranti disambungkan:", "accountId": "ID Akaun:", "systemSetting": "Tetapan Sistem", @@ -282,7 +274,7 @@ const Map msMY = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Kosongkan Sejarah Sembang", "online": "Dalam Talian", "inputMessage": "Masukkan mesej...", @@ -309,7 +301,7 @@ const Map msMY = { "swipeUpForMore": "Leret ke atas untuk lebih banyak", // Agent controller - 'welcomeMessage': 'Helo! Saya **XiaoYan**, saya boleh bantu anda:\n\n' + 'welcomeMessage': 'Helo! Saya **Lily**, saya boleh bantu anda:\n\n' '• Jawab soalan dan berikan maklumat\n\n' '• Terjemahan suara masa nyata\n\n' '• Cadangkan muzik dan podcast\n\n' @@ -698,7 +690,7 @@ const Map msMY = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Pemasangan Peranti", - "connectYourDeepSoundHeadset": "Sambungkan Fon Kepala DeepSound Anda", + "connectYourDeepSoundHeadset": "Sambungkan Fon Kepala Voitrans Anda", "bind": "Ikat", "authorizeBind": "Benarkan Pengikatan", "enterCode": "Sila masukkan kod", @@ -752,7 +744,7 @@ const Map msMY = { 'retryAuthorization': 'Cuba Semula Kebenaran', 'pleaseGrantNecessaryPermissions': 'Sila berikan kebenaran yang diperlukan', 'checkFailedRetry': 'Pemeriksaan gagal, sila cuba lagi', - 'pleaseConnectDeepSoundDevice': 'Sila sambungkan peranti DeepSound', + 'pleaseConnectDeepSoundDevice': 'Sila sambungkan peranti Voitrans', 'verificationFailedRetry': 'Pengesahan gagal, sila cuba lagi', 'invalidDeviceAddress': 'Alamat peranti tidak sah', 'bleDeviceAssociationFailed': 'Perkaitan peranti BLE gagal', diff --git a/lib/core/translations/language/nb_no.dart b/lib/core/translations/language/nb_no.dart index bc001b456..cc35187fb 100644 --- a/lib/core/translations/language/nb_no.dart +++ b/lib/core/translations/language/nb_no.dart @@ -136,7 +136,8 @@ const Map nbNO = { "bluetoothInitializationFailure": "Initialisering av Bluetooth-tjenesten mislyktes", "checkingPermissions": "Sjekker tillatelser...", - "lackPermissions": "Mangler Bluetooth-tillatelser", + "lackPermissions": + "Mangler Bluetooth-tillatelser og er nødvendig å aktivere lokasjons-tillatelser", "searchingDevice": "Søker etter enheter...", "findConnectedDevice": "Fant tilkoblet enhet", "notFound": "Ingen tilkoblet Bluetooth-enhet funnet", @@ -181,22 +182,9 @@ const Map nbNO = { "conferenceAssistant": "Assistent for forretningsmøter", "conferenceAssistantInfo": "Sanntids protokollføring, flerspråklig oversettelse", - "studyAssistant": "Assistent for språklæring", - "studyAssistantInfo": "Uttalekorreksjon, sanntids taleøvelse", - "travelTranslator": "Assistent for reiseoversettelse", - "travelTranslatorInfo": - "Sanntids stemmeoversettelse, introduksjon til turistattraksjoner", - "beSearching": "Søker:", - "noAssistantFound": "Ingen tilsvarende AI-assistent funnet", + "usageStats": "Brukstatistikk", + "usageStatsInfo": "Vis brukstatistikk for din app", "scene": "Scenario", - "youHaveSelected": "Du har valgt:", - "newFunction": "Ny funksjon", - "youChoseTry": "Du valgte å prøve", - "aiSinger": "AI-sangermodus", - "aiSingerInfo": - "Syng hvilken som helst sang med stemmen til din favorittartist, til og med dine egne komposisjoner", - "popularFunction": "Populære funksjonsscenarier", - "tryNow": "Prøv nå", // 商务会议助手页 "startMeeting": "Start møte", @@ -212,8 +200,12 @@ const Map nbNO = { 'meetingTranscript': 'Møtetranskripsjon', // 设置页 - "systemSettings": "Systeminnstillinger", - "premiumMember": "Premium-medlem", + "systemSettings": "Systeminnstillinger", //系统设置 + "vipRecharge": "Premium-medlemsinnskudd", //会员充值 + "aiRecharge": "Premium-medlem", //AI充值 + "translationRecharge": "Oversettelsespenger", //翻译充值 + "meetingAssistantRecharge": "Møteassistentpenger", //会议助手充值 + "cloudSync": "Cloud-synchroniseringspenger", //云端同步 "joinDevice": "Tilkoblet enhet:", "accountId": "Konto-ID:", "systemSetting": "Systeminnstillinger", @@ -283,7 +275,7 @@ const Map nbNO = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Tøm chat-historikk", "online": "Online", "inputMessage": "Skriv melding...", @@ -311,7 +303,7 @@ const Map nbNO = { "swipeUpForMore": "Sveip opp for mer", // Agent controller - 'welcomeMessage': 'Hei! Jeg er **XiaoYan**, jeg kan hjelpe deg med:\n\n' + 'welcomeMessage': 'Hei! Jeg er **Lily**, jeg kan hjelpe deg med:\n\n' '• Å svare på spørsmål og gi informasjon\n\n' '• Sanntids stemmeoversettelse\n\n' '• Å anbefale musikk og podkaster\n\n' @@ -699,7 +691,7 @@ const Map nbNO = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Enhetsparing", - "connectYourDeepSoundHeadset": "Koble til ditt DeepSound-hodesett", + "connectYourDeepSoundHeadset": "Koble til ditt Voitrans-hodesett", "bind": "Bind", "authorizeBind": "Autoriser binding", "enterCode": "Vennligst skriv inn koden", @@ -753,7 +745,7 @@ const Map nbNO = { 'retryAuthorization': 'Prøv autorisasjon på nytt', 'pleaseGrantNecessaryPermissions': 'Vennligst gi nødvendige tillatelser', 'checkFailedRetry': 'Kontroll mislyktes, prøv igjen', - 'pleaseConnectDeepSoundDevice': 'Vennligst koble til en DeepSound-enhet', + 'pleaseConnectDeepSoundDevice': 'Vennligst koble til en Voitrans-enhet', 'verificationFailedRetry': 'Verifisering mislyktes, prøv igjen', 'invalidDeviceAddress': 'Ugyldig enhetsadresse', 'bleDeviceAssociationFailed': 'BLE-enhetsassosiasjon mislyktes', diff --git a/lib/core/translations/language/nl_nl.dart b/lib/core/translations/language/nl_nl.dart index b54cf99d3..d03e9f0ca 100644 --- a/lib/core/translations/language/nl_nl.dart +++ b/lib/core/translations/language/nl_nl.dart @@ -136,7 +136,8 @@ const Map nlNL = { "bluetoothInitializationFailure": "Initialisatie van Bluetooth-service mislukt", "checkingPermissions": "Toestemmingen controleren...", - "lackPermissions": "Bluetooth-toestemmingen ontbreken", + "lackPermissions": + "Bluetooth-toestemmingen ontbreken en zijn nodig om locatie toegang te krijgen", "searchingDevice": "Apparaten zoeken...", "findConnectedDevice": "Verbonden apparaat gevonden", "notFound": "Geen verbonden Bluetooth-apparaat gevonden", @@ -182,22 +183,9 @@ const Map nlNL = { "conferenceAssistant": "Assistent voor zakelijke bijeenkomsten", "conferenceAssistantInfo": "Realtime notuleren van vergaderingen, meertalige vertaling", - "studyAssistant": "Assistent voor taalstudie", - "studyAssistantInfo": "Uitspraakcorrectie, realtime spraakoefening", - "travelTranslator": "Assistent voor reisvertaling", - "travelTranslatorInfo": - "Realtime spraakvertaling, introductie van toeristische attracties", - "beSearching": "Zoeken:", - "noAssistantFound": "Geen overeenkomende AI-assistent gevonden", + "usageStats": "Gebruikstatistieken", + "usageStatsInfo": " Bekijk de gebruikstatistieken van uw applicatie", "scene": "Scenario", - "youHaveSelected": "U heeft geselecteerd:", - "newFunction": "Nieuwe functie", - "youChoseTry": "U koos ervoor om te proberen", - "aiSinger": "AI-zangermodus", - "aiSingerInfo": - "Zing elk nummer met de stem van uw favoriete zanger, zelfs uw eigen composities", - "popularFunction": "Populaire functiescenario's", - "tryNow": "Probeer nu", // 商务会议助手页 "startMeeting": "Start vergadering", @@ -213,8 +201,12 @@ const Map nlNL = { 'meetingTranscript': 'Vergadertranscript', // 设置页 - "systemSettings": "Systeeminstellingen", - "premiumMember": "Premium lid", + "systemSettings": "Systeeminstellingen", //系统设置 + "vipRecharge": "Premium lidspayering", //会员充值 + "aiRecharge": "Premium lid", //AI充值 + "translationRecharge": "Vertalingspayering", //翻译充值 + "meetingAssistantRecharge": "Vergaderassistentpayering", //会议助手充值 + "cloudSync": "Cloud-synchronisatiepayering", //云端同步 "joinDevice": "Verbonden apparaat:", "accountId": "Account-ID:", "systemSetting": "Systeeminstellingen", @@ -283,7 +275,7 @@ const Map nlNL = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Chatgeschiedenis wissen", "online": "Online", "inputMessage": "Voer bericht in...", @@ -311,7 +303,7 @@ const Map nlNL = { "swipeUpForMore": "Veeg omhoog voor meer", // Agent controller - 'welcomeMessage': 'Hallo! Ik ben **XiaoYan**, ik kan u helpen met:\n\n' + 'welcomeMessage': 'Hallo! Ik ben **Lily**, ik kan u helpen met:\n\n' '• Vragen beantwoorden en informatie verstrekken\n\n' '• Realtime spraakvertaling\n\n' '• Muziek en podcasts aanbevelen\n\n' @@ -705,7 +697,7 @@ const Map nlNL = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Apparaat koppelen", - "connectYourDeepSoundHeadset": "Verbind uw DeepSound-headset", + "connectYourDeepSoundHeadset": "Verbind uw Voitrans-headset", "bind": "Koppelen", "authorizeBind": "Koppeling autoriseren", "enterCode": "Voer de code in", @@ -760,7 +752,7 @@ const Map nlNL = { 'retryAuthorization': 'Autorisatie opnieuw proberen', 'pleaseGrantNecessaryPermissions': 'Verleen de benodigde toestemmingen', 'checkFailedRetry': 'Controle mislukt, probeer het opnieuw', - 'pleaseConnectDeepSoundDevice': 'Verbind een DeepSound-apparaat', + 'pleaseConnectDeepSoundDevice': 'Verbind een Voitrans-apparaat', 'verificationFailedRetry': 'Verificatie mislukt, probeer het opnieuw', 'invalidDeviceAddress': 'Ongeldig apparaatadres', 'bleDeviceAssociationFailed': 'Associatie van BLE-apparaat mislukt', diff --git a/lib/core/translations/language/pl_pl.dart b/lib/core/translations/language/pl_pl.dart index 051c10d78..c00f8046d 100644 --- a/lib/core/translations/language/pl_pl.dart +++ b/lib/core/translations/language/pl_pl.dart @@ -136,7 +136,8 @@ const Map plPL = { "bluetoothInitializationFailure": "Inicjalizacja usługi Bluetooth nie powiodła się", "checkingPermissions": "Sprawdzanie uprawnień...", - "lackPermissions": "Brak uprawnień Bluetooth", + "lackPermissions": + "Brak uprawnień Bluetooth i jest wymagana prawo dostępu do lokalizacji", "searchingDevice": "Wyszukiwanie urządzeń...", "findConnectedDevice": "Znaleziono podłączone urządzenie", "notFound": "Nie znaleziono podłączonego urządzenia Bluetooth", @@ -183,22 +184,9 @@ const Map plPL = { "conferenceAssistant": "Asystent spotkań biznesowych", "conferenceAssistantInfo": "Zapisywanie protokołów w czasie rzeczywistym, tłumaczenie wielojęzyczne", - "studyAssistant": "Asystent nauki języków", - "studyAssistantInfo": "Korekta wymowy, ćwiczenie mowy w czasie rzeczywistym", - "travelTranslator": "Asystent tłumaczenia w podróży", - "travelTranslatorInfo": - "Tłumaczenie głosowe w czasie rzeczywistym, prezentacja atrakcji turystycznych", - "beSearching": "Wyszukiwanie:", - "noAssistantFound": "Nie znaleziono odpowiedniego asystenta AI", + "usageStats": "Statystyki użycia", + "usageStatsInfo": "Przeglądaj statystyki użycia aplikacji", "scene": "Scenariusz", - "youHaveSelected": "Wybrałeś:", - "newFunction": "Nowa funkcja", - "youChoseTry": "Wybrałeś, aby spróbować", - "aiSinger": "Tryb piosenkarza AI", - "aiSingerInfo": - "Śpiewaj dowolną piosenkę głosem ulubionego piosenkarza, nawet własne kompozycje", - "popularFunction": "Popularne scenariusze funkcji", - "tryNow": "Wypróbuj teraz", // 商务会议助手页 "startMeeting": "Rozpocznij spotkanie", @@ -214,8 +202,12 @@ const Map plPL = { 'meetingTranscript': 'Transkrypcja spotkania', // 设置页 - "systemSettings": "Ustawienia systemowe", - "premiumMember": "Członek Premium", + "systemSettings": "Ustawienia systemowe", //系统设置 + "vipRecharge": "Płatność Premium", //会员充值 + "aiRecharge": "Członek Premium", //AI充值 + "translationRecharge": "Płatność tłumaczenia", //翻译充值 + "meetingAssistantRecharge": "Płatność asystenta spotkań", //会议助手充值 + "cloudSync": "Płatność synchronizacji w chmurze", //云端同步 "joinDevice": "Podłączone urządzenie:", "accountId": "ID konta:", "systemSetting": "Ustawienia systemowe", @@ -286,7 +278,7 @@ const Map plPL = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Wyczyść historię czatu", "online": "Online", "inputMessage": "Wpisz wiadomość...", @@ -314,7 +306,7 @@ const Map plPL = { "swipeUpForMore": "Przesuń w górę, aby zobaczyć więcej", // Agent controller - 'welcomeMessage': 'Cześć! Jestem **XiaoYan**, mogę Ci pomóc:\n\n' + 'welcomeMessage': 'Cześć! Jestem **Lily**, mogę Ci pomóc:\n\n' '• Odpowiadać na pytania i dostarczać informacji\n\n' '• Tłumaczyć głosowo w czasie rzeczywistym\n\n' '• Polecać muzykę i podcasty\n\n' @@ -709,7 +701,7 @@ const Map plPL = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Parowanie urządzenia", - "connectYourDeepSoundHeadset": "Połącz swoje słuchawki DeepSound", + "connectYourDeepSoundHeadset": "Połącz swoje słuchawki Voitrans", "bind": "Powiąż", "authorizeBind": "Autoryzuj powiązanie", "enterCode": "Wprowadź kod", @@ -765,7 +757,7 @@ const Map plPL = { 'retryAuthorization': 'Spróbuj ponownie autoryzować', 'pleaseGrantNecessaryPermissions': 'Udziel niezbędnych uprawnień', 'checkFailedRetry': 'Sprawdzenie nie powiodło się, spróbuj ponownie', - 'pleaseConnectDeepSoundDevice': 'Połącz urządzenie DeepSound', + 'pleaseConnectDeepSoundDevice': 'Połącz urządzenie Voitrans', 'verificationFailedRetry': 'Weryfikacja nie powiodła się, spróbuj ponownie', 'invalidDeviceAddress': 'Nieprawidłowy adres urządzenia', 'bleDeviceAssociationFailed': 'Skojarzenie urządzenia BLE nie powiodło się', diff --git a/lib/core/translations/language/pt_pt.dart b/lib/core/translations/language/pt_pt.dart index 14eadd866..4404d9d2b 100644 --- a/lib/core/translations/language/pt_pt.dart +++ b/lib/core/translations/language/pt_pt.dart @@ -137,7 +137,8 @@ const Map ptPT = { "bluetoothInitializationFailure": "Falha na inicialização do serviço Bluetooth", "checkingPermissions": "A verificar permissões...", - "lackPermissions": "Faltam permissões de Bluetooth", + "lackPermissions": + "Faltam permissões de Bluetooth e é necessário permitir o acesso à localização", "searchingDevice": "A procurar dispositivos...", "findConnectedDevice": "Dispositivo conectado encontrado", "notFound": "Nenhum dispositivo Bluetooth conectado encontrado", @@ -185,23 +186,17 @@ const Map ptPT = { "conferenceAssistant": "Assistente de Reunião de Negócios", "conferenceAssistantInfo": "Gravação de atas em tempo real, tradução multilingue", - "studyAssistant": "Assistente de Aprendizagem de Línguas", - "studyAssistantInfo": - "Correção de pronúncia, prática de conversação em tempo real", - "travelTranslator": "Assistente de Tradução de Viagem", - "travelTranslatorInfo": - "Tradução de voz em tempo real, apresentação de pontos turísticos", - "beSearching": "A pesquisar:", - "noAssistantFound": "Nenhum assistente de IA correspondente encontrado", + "usageStats": "Estatísticas de Uso", + "usageStatsInfo": "Visualize as estatísticas de uso da sua aplicação", "scene": "Cenário", - "youHaveSelected": "Selecionou:", - "newFunction": "Nova Função", - "youChoseTry": "Escolheu experimentar", - "aiSinger": "Modo Cantor IA", - "aiSingerInfo": - "Cante qualquer música com a voz do seu cantor favorito, até mesmo as suas próprias composições", - "popularFunction": "Cenários de Funções Populares", - "tryNow": "Experimente Agora", + + 'trans_001_name': 'Tradução Básica 100', + 'trans_002_name': 'Tradução Básica 500', + 'trans_003_name': 'Tradução Básica 1000', + + 'trans_001_desc': 'Inclui 100 traduções (voz/texto).', + 'trans_002_desc': 'Inclui 500 traduções (voz/texto).', + 'trans_003_desc': 'Inclui 1000 traduções (voz/texto).', // 商务会议助手页 "startMeeting": "Iniciar Reunião", @@ -217,8 +212,12 @@ const Map ptPT = { 'meetingTranscript': 'Transcrição da Reunião', // 设置页 - "systemSettings": "Definições do Sistema", - "premiumMember": "Membro Premium", + "systemSettings": "Definições do Sistema", //系统设置 + "vipRecharge": "Recarga Premium", //会员充值 + "aiRecharge": "Membro Premium", //AI充值 + "translationRecharge": "Recarga de Tradução", //翻译充值 + "meetingAssistantRecharge": "Recarga do Assistente de Reunião", //会议助手充值 + "cloudSync": "Recarga de Sincronização em Nuvem", //云端同步 "joinDevice": "Dispositivo conectado:", "accountId": "ID da Conta:", "systemSetting": "Definição do Sistema", @@ -289,7 +288,7 @@ const Map ptPT = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Limpar Histórico de Chat", "online": "Online", "inputMessage": "Introduzir mensagem...", @@ -317,7 +316,7 @@ const Map ptPT = { "swipeUpForMore": "Deslize para cima para ver mais", // Agent controller - 'welcomeMessage': 'Olá! Eu sou a **XiaoYan**, posso ajudá-lo(a) a:\n\n' + 'welcomeMessage': 'Olá! Eu sou a **Lily**, posso ajudá-lo(a) a:\n\n' '• Responder a perguntas e fornecer informações\n\n' '• Traduzir voz em tempo real\n\n' '• Recomendar música e podcasts\n\n' @@ -711,7 +710,7 @@ const Map ptPT = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Emparelhamento de Dispositivo", - "connectYourDeepSoundHeadset": "Conecte os seus Auriculares DeepSound", + "connectYourDeepSoundHeadset": "Conecte os seus Auriculares Voitrans", "bind": "Vincular", "authorizeBind": "Autorizar Vinculação", "enterCode": "Introduza o código", @@ -768,7 +767,7 @@ const Map ptPT = { 'pleaseGrantNecessaryPermissions': 'Por favor, conceda as permissões necessárias', 'checkFailedRetry': 'Verificação falhou, tente novamente', - 'pleaseConnectDeepSoundDevice': 'Por favor, conecte um dispositivo DeepSound', + 'pleaseConnectDeepSoundDevice': 'Por favor, conecte um dispositivo Voitrans', 'verificationFailedRetry': 'Verificação falhou, tente novamente', 'invalidDeviceAddress': 'Endereço de dispositivo inválido', 'bleDeviceAssociationFailed': 'Falha na associação do dispositivo BLE', @@ -863,4 +862,90 @@ const Map ptPT = { "乌尔都语": "Urdu", "波斯语": "Persa", "斯瓦希里语": "Suaíli", + // Guide overlay keys (placeholders in English) + "sayLilyHint": "Say \"Lily Lily\" to the mic", + "imHere": "I’m here~", + "guideWelcomeWakeAI": "Welcome to Voitrans! First, wake your AI “Lily”.", + "aiReplyCompleted": "Lily has finished the reply!", + "aiThinking": "Lily is thinking…", + "guideAwakenSuccess": "Great! Lily is awake—ask it to help.", + "confirmInput": "Confirm Input", + "guideClickSendPrefix": "Tap send, ", + "guideWeatherStart": "Tell Lily: “What’s the weather today?”", + "guideWeatherInput": "What’s the weather today?", + "guideWeatherSend": "Lily will report the weather in real time", + "guideWeatherEnd": "Lily can report weather live—try other commands", + "guideMusicStart": "Tell Lily: “Play some rock music”", + "guideMusicInput": "Play some rock music", + "guideMusicSend": "Lily will find music for you", + "guideMusicEnd": "Lily found music—try other commands", + "guideNavStart": "Tell Lily: “Navigate to Shenzhen Airport”", + "guideNavInput": "Navigate to Shenzhen Airport", + "guideNavSend": "Lily will plan the route", + "guideNavEnd": "Navigation set—try other commands", + "guideMeetingStart": + "Tell Lily: “Create a meeting reminder for 3pm tomorrow”", + "guideMeetingInput": "Create a meeting reminder for 3pm tomorrow", + "guideMeetingSend": "Lily will create a meeting reminder", + "guideMeetingEnd": "Reminder created—try other commands", + "guidePhoneStart": "Tell Lily: “Call Mom”", + "guidePhoneInput": "Call Mom", + "guidePhoneSend": "Lily will make the call", + "guidePhoneEnd": "Call placed—try other commands", + "guideSmsStart": "Tell Lily: “Send Dad a text: I’ll be home for dinner”", + "guideSmsInput": "Text Dad: I’ll be home for dinner", + "guideSmsSend": "Lily will send the message", + "guideSmsEnd": "Message sent—voice control is convenient", + // Goods pages common footer (placeholders) + 'appNameProvider': 'App: VoiTrans · Provided by: VoiTrans', + 'copyrightVoitrans': '© 2025 Voitrans Technologies', + + // Meeting goods page (placeholders) + 'meetingHeaderDesc1': + 'Audio recording, transcription, keyword search, and auto summary.', + 'meetingHeaderDesc2': + 'Suitable for business meetings, class notes, and interview minutes.', + 'meetingAutoRecording': 'Auto recording and segmentation', + 'meetingSpeakerTagging': 'Speaker labeling', + 'meetingLongAudioTranscription': 'Long audio transcription', + 'meetingKeyPointSummary': 'Key point extraction', + 'meetingTopicNameSearch': 'Search by topic/name', + 'meetingTimelineJump': 'Timeline jump', + 'meetingExportSummary': 'Export minutes', + 'meetingMultiPlatformShare': 'Share across platforms', + 'meetingDisclaimer': + 'Use in quiet environments; performance depends on microphone and network.', + + // Translation goods page (placeholders) + 'transHeaderDesc1': + 'Interpretação simultânea e tradução de texto em tempo real, com voz de baixa latência.', + 'transHeaderDesc2': 'Adequado para viagens, negócios, palestras e ensino.', + 'translateZhEn': 'Tradução Chinês-Inglês', + 'lowLatencyVoiceBroadcast': 'Transmissão de voz de baixa latência', + 'multiLangTextTranslation': 'Tradução de texto multilingue', + 'termOptimizationContextPreserve': + 'Otimização de terminologia e preservação de contexto', + 'lexiconTourBusinessLife': 'Léxicos de viagem/negócios/vida', + 'personalVocabularyMemory': 'Memória de vocabulário pessoal', + 'conversationSubtitleLargeText': + 'Legendagem de conversas / modo de texto grande', + 'colorHighContrast': 'Alto contraste de cor', + 'transDisclaimer': + 'O desempenho da tradução depende do microfone, da latência da rede e do ruído ambiente.', + + // VIP goods page (placeholders) + 'benefitsOverview': 'Visão geral de benefícios', + 'higherQuota': 'Maior quota de utilização', + 'lowerUnitPrice': 'Preço unitário inferior', + 'priorityService': 'Serviço prioritário', + 'exclusiveSupportChannel': 'Canal de suporte dedicado', + 'fastResponse': 'Resposta rápida a problemas', + 'eventsAndDiscounts': 'Eventos e descontos', + 'memberExclusiveEvents': 'Eventos exclusivos para membros', + 'longTermDiscountPlan': 'Plano de desconto a longo prazo', + 'generalPrivileges': 'Privilégios gerais', + 'earlyAccessNewFeatures': 'Acesso antecipado a novas funcionalidades', + 'crossDeviceSync': 'Sincronização entre dispositivos', + 'vipDisclaimer': + 'Os benefícios reais dependem dos detalhes do pacote e podem mudar entre versões.', }; diff --git a/lib/core/translations/language/ro_ro.dart b/lib/core/translations/language/ro_ro.dart index 2184bc439..fe9b4158a 100644 --- a/lib/core/translations/language/ro_ro.dart +++ b/lib/core/translations/language/ro_ro.dart @@ -137,7 +137,8 @@ const Map roRO = { "bluetoothInitializationFailure": "Inițializarea serviciului Bluetooth a eșuat", "checkingPermissions": "Se verifică permisiunile...", - "lackPermissions": "Lipsesc permisiunile Bluetooth", + "lackPermissions": + "Lipsesc permisiunile Bluetooth și este necesar să permiteți accesul la localizare", "searchingDevice": "Se caută dispozitive...", "findConnectedDevice": "Dispozitiv conectat găsit", "notFound": "Niciun dispozitiv Bluetooth conectat nu a fost găsit", @@ -184,23 +185,9 @@ const Map roRO = { "conferenceAssistant": "Asistent pentru ședințe de afaceri", "conferenceAssistantInfo": "Înregistrare în timp real a proceselor-verbale, traducere multilingvă", - "studyAssistant": "Asistent pentru învățarea limbilor străine", - "studyAssistantInfo": - "Corectarea pronunției, practicarea vorbirii în timp real", - "travelTranslator": "Asistent de traducere pentru călătorii", - "travelTranslatorInfo": - "Traducere vocală în timp real, prezentarea atracțiilor turistice", - "beSearching": "Se caută:", - "noAssistantFound": "Niciun asistent AI corespunzător nu a fost găsit", + "usageStats": "Statistici de utilizare", + "usageStatsInfo": "Vizualizați istoricul utilizării aplicației", "scene": "Scenariu", - "youHaveSelected": "Ați selectat:", - "newFunction": "Funcție nouă", - "youChoseTry": "Ați ales să încercați", - "aiSinger": "Modul cântăreț AI", - "aiSingerInfo": - "Cântați orice melodie cu vocea cântărețului preferat, chiar și propriile compoziții", - "popularFunction": "Scenarii de funcții populare", - "tryNow": "Încearcă acum", // 商务会议助手页 "startMeeting": "Începe ședința", @@ -216,8 +203,12 @@ const Map roRO = { 'meetingTranscript': 'Transcriere ședință', // 设置页 - "systemSettings": "Setări de sistem", - "premiumMember": "Membru Premium", + "systemSettings": "Setări de sistem", //系统设置 + "vipRecharge": "Recarga Premium", //会员充值 + "aiRecharge": "Membru Premium", //AI充值 + "translationRecharge": "Recarga de traducere", //翻译充值 + "meetingAssistantRecharge": "Recarga de asistent ședință", //会议助手充值 + "cloudSync": "Recarga de sincronizare în cloud", //云端同步 "joinDevice": "Dispozitiv conectat:", "accountId": "ID cont:", "systemSetting": "Setări de sistem", @@ -288,7 +279,7 @@ const Map roRO = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Șterge istoricul chatului", "online": "Online", "inputMessage": "Introduceți mesaj...", @@ -316,7 +307,7 @@ const Map roRO = { "swipeUpForMore": "Glisați în sus pentru mai mult", // Agent controller - 'welcomeMessage': 'Bună! Sunt **XiaoYan**, vă pot ajuta cu:\n\n' + 'welcomeMessage': 'Bună! Sunt **Lily**, vă pot ajuta cu:\n\n' '• Răspuns la întrebări și furnizare de informații\n\n' '• Traducere vocală în timp real\n\n' '• Recomandare de muzică și podcasturi\n\n' @@ -713,7 +704,7 @@ const Map roRO = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Asociere dispozitiv", - "connectYourDeepSoundHeadset": "Conectați-vă căștile DeepSound", + "connectYourDeepSoundHeadset": "Conectați-vă căștile Voitrans", "bind": "Leagă", "authorizeBind": "Autorizează legarea", "enterCode": "Introduceți codul", @@ -768,7 +759,7 @@ const Map roRO = { 'retryAuthorization': 'Reîncearcă autorizarea', 'pleaseGrantNecessaryPermissions': 'Acordați permisiunile necesare', 'checkFailedRetry': 'Verificare eșuată, reîncercați', - 'pleaseConnectDeepSoundDevice': 'Conectați un dispozitiv DeepSound', + 'pleaseConnectDeepSoundDevice': 'Conectați un dispozitiv Voitrans', 'verificationFailedRetry': 'Verificare eșuată, reîncercați', 'invalidDeviceAddress': 'Adresă de dispozitiv invalidă', 'bleDeviceAssociationFailed': 'Asocierea dispozitivului BLE a eșuat', diff --git a/lib/core/translations/language/ru_ru.dart b/lib/core/translations/language/ru_ru.dart index 6d6603e5d..95674fdc5 100644 --- a/lib/core/translations/language/ru_ru.dart +++ b/lib/core/translations/language/ru_ru.dart @@ -137,7 +137,8 @@ const Map ruRU = { "initializing": "Инициализация...", "bluetoothInitializationFailure": "Ошибка инициализации службы Bluetooth", "checkingPermissions": "Проверка разрешений...", - "lackPermissions": "Отсутствуют разрешения Bluetooth", + "lackPermissions": + "Отсутствуют разрешения Bluetooth и необходимо разрешить доступ к местоположению", "searchingDevice": "Поиск устройств...", "findConnectedDevice": "Найдено подключенное устройство", "notFound": "Подключенное устройство Bluetooth не найдено", @@ -183,23 +184,17 @@ const Map ruRU = { "conferenceAssistant": "Помощник для деловых встреч", "conferenceAssistantInfo": "Запись протоколов в реальном времени, многоязычный перевод", - "studyAssistant": "Помощник в изучении языков", - "studyAssistantInfo": - "Коррекция произношения, практика разговорной речи в реальном времени", - "travelTranslator": "Помощник-переводчик для путешествий", - "travelTranslatorInfo": - "Голосовой перевод в реальном времени, описание туристических достопримечательностей", - "beSearching": "Поиск:", - "noAssistantFound": "Соответствующий AI-помощник не найден", + "usageStats": "Статистика использования", + "usageStatsInfo": "Просмотрите статистику использования вашего приложения", "scene": "Сценарий", - "youHaveSelected": "Вы выбрали:", - "newFunction": "Новая функция", - "youChoseTry": "Вы решили попробовать", - "aiSinger": "Режим AI-певца", - "aiSingerInfo": - "Пойте любую песню голосом вашего любимого исполнителя, даже ваши собственные композиции", - "popularFunction": "Популярные функциональные сценарии", - "tryNow": "Попробовать сейчас", + + 'trans_001_name': 'Перевод Базовый 100', + 'trans_002_name': 'Перевод Базовый 500', + 'trans_003_name': 'Перевод Базовый 1000', + + 'trans_001_desc': 'Включает 100 переводов (голос/текст).', + 'trans_002_desc': 'Включает 500 переводов (голос/текст).', + 'trans_003_desc': 'Включает 1000 переводов (голос/текст).', // 商务会议助手页 "startMeeting": "Начать встречу", @@ -215,8 +210,12 @@ const Map ruRU = { 'meetingTranscript': 'Транскрипт встречи', // 设置页 - "systemSettings": "Системные настройки", - "premiumMember": "Премиум-участник", + "systemSettings": "Системные настройки", //系统设置 + "vipRecharge": "Пополнение премиум-члена", //会员充值 + "aiRecharge": "Премиум-участник", //AI充值 + "translationRecharge": "Пополнение перевода", //翻译充值 + "meetingAssistantRecharge": "Пополнение помощника встречи", //会议助手充值 + "cloudSync": "Пополнение синхронизации в облаке", //云端同步 "joinDevice": "Подключенное устройство:", "accountId": "ID аккаунта:", "systemSetting": "Системные настройки", @@ -285,7 +284,7 @@ const Map ruRU = { //Agent 模块 // agent View - "XiaoYan": "СяоЯнь", + "Lily": "Lily", "clearChatHistory": "Очистить историю чата", "online": "В сети", "inputMessage": "Введите сообщение...", @@ -761,7 +760,7 @@ const Map ruRU = { 'pleaseGrantNecessaryPermissions': 'Пожалуйста, предоставьте необходимые разрешения', 'checkFailedRetry': 'Проверка не удалась, попробуйте еще раз', - 'pleaseConnectDeepSoundDevice': 'Пожалуйста, подключите устройство DeepSound', + 'pleaseConnectDeepSoundDevice': 'Пожалуйста, подключите устройство Voitrans', 'verificationFailedRetry': 'Проверка не удалась, попробуйте еще раз', 'invalidDeviceAddress': 'Неверный адрес устройства', 'bleDeviceAssociationFailed': 'Не удалось связать устройство BLE', @@ -856,4 +855,91 @@ const Map ruRU = { "乌尔都语": "Урду", "波斯语": "Персидский", "斯瓦希里语": "Суахили", + // Guide overlay keys (placeholders in English) + "sayLilyHint": "Say \"Lily Lily\" to the mic", + "imHere": "I’m here~", + "guideWelcomeWakeAI": "Welcome to Voitrans! First, wake your AI “Lily”.", + "aiReplyCompleted": "Lily has finished the reply!", + "aiThinking": "Lily is thinking…", + "guideAwakenSuccess": "Great! Lily is awake—ask it to help.", + "confirmInput": "Confirm Input", + "guideClickSendPrefix": "Tap send, ", + "guideWeatherStart": "Tell Lily: “What’s the weather today?”", + "guideWeatherInput": "What’s the weather today?", + "guideWeatherSend": "Lily will report the weather in real time", + "guideWeatherEnd": "Lily can report weather live—try other commands", + "guideMusicStart": "Tell Lily: “Play some rock music”", + "guideMusicInput": "Play some rock music", + "guideMusicSend": "Lily will find music for you", + "guideMusicEnd": "Lily found music—try other commands", + "guideNavStart": "Tell Lily: “Navigate to Shenzhen Airport”", + "guideNavInput": "Navigate to Shenzhen Airport", + "guideNavSend": "Lily will plan the route", + "guideNavEnd": "Navigation set—try other commands", + "guideMeetingStart": + "Tell Lily: “Create a meeting reminder for 3pm tomorrow”", + "guideMeetingInput": "Create a meeting reminder for 3pm tomorrow", + "guideMeetingSend": "Lily will create a meeting reminder", + "guideMeetingEnd": "Reminder created—try other commands", + "guidePhoneStart": "Tell Lily: “Call Mom”", + "guidePhoneInput": "Call Mom", + "guidePhoneSend": "Lily will make the call", + "guidePhoneEnd": "Call placed—try other commands", + "guideSmsStart": "Tell Lily: “Send Dad a text: I’ll be home for dinner”", + "guideSmsInput": "Text Dad: I’ll be home for dinner", + "guideSmsSend": "Lily will send the message", + "guideSmsEnd": "Message sent—voice control is convenient", + // Goods pages common footer (placeholders) + 'appNameProvider': 'App: VoiTrans · Provided by: VoiTrans', + 'copyrightVoitrans': '© 2025 Voitrans Technologies', + + // Meeting goods page (placeholders) + 'meetingHeaderDesc1': + 'Audio recording, transcription, keyword search, and auto summary.', + 'meetingHeaderDesc2': + 'Suitable for business meetings, class notes, and interview minutes.', + 'meetingAutoRecording': 'Auto recording and segmentation', + 'meetingSpeakerTagging': 'Speaker labeling', + 'meetingLongAudioTranscription': 'Long audio transcription', + 'meetingKeyPointSummary': 'Key point extraction', + 'meetingTopicNameSearch': 'Search by topic/name', + 'meetingTimelineJump': 'Timeline jump', + 'meetingExportSummary': 'Export minutes', + 'meetingMultiPlatformShare': 'Share across platforms', + 'meetingDisclaimer': + 'Use in quiet environments; performance depends on microphone and network.', + + // Translation goods page (placeholders) + 'transHeaderDesc1': + 'Синхронный перевод и перевод текста в реальном времени с низкой задержкой голосового озвучивания.', + 'transHeaderDesc2': + 'Подходит для путешествий, бизнеса, выступлений и обучения.', + 'translateZhEn': 'Китайско-английский перевод', + 'lowLatencyVoiceBroadcast': 'Голосовое вещание с низкой задержкой', + 'multiLangTextTranslation': 'Многоязычный перевод текста', + 'termOptimizationContextPreserve': + 'Оптимизация терминологии и сохранение контекста', + 'lexiconTourBusinessLife': + 'Лексика для путешествий/бизнеса/повседневной жизни', + 'personalVocabularyMemory': 'Память персонального словаря', + 'conversationSubtitleLargeText': 'Субтитры беседы / режим крупных шрифтов', + 'colorHighContrast': 'Высокая контрастность цветов', + 'transDisclaimer': + 'Качество перевода зависит от микрофона, сетевой задержки и фонового шума.', + + // VIP goods page (placeholders) + 'benefitsOverview': 'Обзор преимуществ', + 'higherQuota': 'Более высокая квота использования', + 'lowerUnitPrice': 'Низкая цена за единицу', + 'priorityService': 'Приоритетное обслуживание', + 'exclusiveSupportChannel': 'Выделенный канал поддержки', + 'fastResponse': 'Быстрый отклик на проблемы', + 'eventsAndDiscounts': 'События и скидки', + 'memberExclusiveEvents': 'Эксклюзивные мероприятия для участников', + 'longTermDiscountPlan': 'Долгосрочный план скидок', + 'generalPrivileges': 'Общие привилегии', + 'earlyAccessNewFeatures': 'Ранний доступ к новым функциям', + 'crossDeviceSync': 'Синхронизация между устройствами', + 'vipDisclaimer': + 'Фактические преимущества зависят от деталей пакета и могут меняться с версиями.', }; diff --git a/lib/core/translations/language/sk_sk.dart b/lib/core/translations/language/sk_sk.dart index a20e350c8..2e33a37ad 100644 --- a/lib/core/translations/language/sk_sk.dart +++ b/lib/core/translations/language/sk_sk.dart @@ -136,7 +136,8 @@ const Map skSK = { "initializing": "Inicializácia...", "bluetoothInitializationFailure": "Inicializácia služby Bluetooth zlyhala", "checkingPermissions": "Kontrola povolení...", - "lackPermissions": "Chýbajú povolenia Bluetooth", + "lackPermissions": + "Chýbajú povolenia Bluetooth a je potrebné povoliť prístup k lokáli", "searchingDevice": "Vyhľadávanie zariadení...", "findConnectedDevice": "Nájdené pripojené zariadenie", "notFound": "Nenájdené žiadne pripojené Bluetooth zariadenie", @@ -182,22 +183,9 @@ const Map skSK = { "conferenceAssistant": "Asistent pre obchodné stretnutia", "conferenceAssistantInfo": "Záznam zápisnice v reálnom čase, viacjazyčný preklad", - "studyAssistant": "Asistent pre štúdium jazykov", - "studyAssistantInfo": "Oprava výslovnosti, prax reči v reálnom čase", - "travelTranslator": "Asistent pre preklad na cestách", - "travelTranslatorInfo": - "Hlasový preklad v reálnom čase, predstavenie turistických atrakcií", - "beSearching": "Hľadám:", - "noAssistantFound": "Nenájdený žiadny zodpovedajúci AI asistent", + "usageStats": "Statistiky využitia", + "usageStatsInfo": "Zobrazte vaše využitie aplikácie", "scene": "Scenár", - "youHaveSelected": "Vybrali ste:", - "newFunction": "Nová funkcia", - "youChoseTry": "Rozhodli ste sa vyskúšať", - "aiSinger": "Režim AI speváka", - "aiSingerInfo": - "Spievajte akúkoľvek pieseň hlasom vášho obľúbeného speváka, dokonca aj vlastné skladby", - "popularFunction": "Populárne funkčné scenáre", - "tryNow": "Vyskúšať teraz", // 商务会议助手页 "startMeeting": "Začať stretnutie", @@ -213,8 +201,12 @@ const Map skSK = { 'meetingTranscript': 'Prepis stretnutia', // 设置页 - "systemSettings": "Systémové nastavenia", - "premiumMember": "Prémium člen", + "systemSettings": "Systémové nastavenia", //系统设置 + "vipRecharge": "Nákup prémium členstva", //会员充值 + "aiRecharge": "Prémium člen", //AI充值 + "translationRecharge": "Nákup prekladov", //翻译充值 + "meetingAssistantRecharge": "Nákup asistentu stretnutia", //会议助手充值 + "cloudSync": "Nákup synchronizácie v cloude", //云端同步 "joinDevice": "Pripojené zariadenie:", "accountId": "ID účtu:", "systemSetting": "Systémové nastavenie", @@ -283,7 +275,7 @@ const Map skSK = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Vymazať históriu chatu", "online": "Online", "inputMessage": "Zadajte správu...", @@ -310,7 +302,7 @@ const Map skSK = { "swipeUpForMore": "Potiahnite nahor pre viac", // Agent controller - 'welcomeMessage': 'Ahoj! Som **XiaoYan**, môžem vám pomôcť:\n\n' + 'welcomeMessage': 'Ahoj! Som **Lily**, môžem vám pomôcť:\n\n' '• Odpovedať na otázky a poskytovať informácie\n\n' '• Hlasový preklad v reálnom čase\n\n' '• Odporúčať hudbu a podcasty\n\n' @@ -699,7 +691,7 @@ const Map skSK = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Párovanie zariadenia", - "connectYourDeepSoundHeadset": "Pripojte svoje slúchadlá DeepSound", + "connectYourDeepSoundHeadset": "Pripojte svoje slúchadlá Voitrans", "bind": "Prepojiť", "authorizeBind": "Autorizovať prepojenie", "enterCode": "Zadajte kód", @@ -754,7 +746,7 @@ const Map skSK = { 'retryAuthorization': 'Znova autorizovať', 'pleaseGrantNecessaryPermissions': 'Udeľte potrebné povolenia', 'checkFailedRetry': 'Kontrola zlyhala, skúste to znova', - 'pleaseConnectDeepSoundDevice': 'Pripojte zariadenie DeepSound', + 'pleaseConnectDeepSoundDevice': 'Pripojte zariadenie Voitrans', 'verificationFailedRetry': 'Overenie zlyhalo, skúste to znova', 'invalidDeviceAddress': 'Neplatná adresa zariadenia', 'bleDeviceAssociationFailed': 'Priradenie zariadenia BLE zlyhalo', diff --git a/lib/core/translations/language/sl_si.dart b/lib/core/translations/language/sl_si.dart index d106107e0..4bd16b7b9 100644 --- a/lib/core/translations/language/sl_si.dart +++ b/lib/core/translations/language/sl_si.dart @@ -136,7 +136,8 @@ const Map slSI = { "bluetoothInitializationFailure": "Inicializacija storitve Bluetooth ni uspela", "checkingPermissions": "Preverjanje dovoljenj...", - "lackPermissions": "Manjkajo dovoljenja za Bluetooth", + "lackPermissions": + "Manjkajo dovoljenja za Bluetooth in je potrebno omogočiti dostop do lokacije", "searchingDevice": "Iskanje naprav...", "findConnectedDevice": "Najdena povezana naprava", "notFound": "Nobena povezana naprava Bluetooth ni bila najdena", @@ -182,22 +183,9 @@ const Map slSI = { "conferenceAssistant": "Pomočnik za poslovne sestanke", "conferenceAssistantInfo": "Zapisovanje zapisnikov v realnem času, večjezično prevajanje", - "studyAssistant": "Pomočnik za učenje jezikov", - "studyAssistantInfo": "Popravek izgovorjave, vadba govorjenja v realnem času", - "travelTranslator": "Pomočnik za prevajanje na potovanjih", - "travelTranslatorInfo": - "Glasovno prevajanje v realnem času, predstavitev turističnih znamenitosti", - "beSearching": "Iskanje:", - "noAssistantFound": "Ni bil najden noben ustrezen pomočnik AI", + "usageStats": "Statistika uporabe", + "usageStatsInfo": "Pregledajte vaše uporabo aplikacije", "scene": "Scenarij", - "youHaveSelected": "Izbrali ste:", - "newFunction": "Nova funkcija", - "youChoseTry": "Izbrali ste poskusiti", - "aiSinger": "Način AI pevca", - "aiSingerInfo": - "Zapojte katero koli pesem z glasom vašega najljubšega pevca, celo lastne skladbe", - "popularFunction": "Priljubljeni funkcijski scenariji", - "tryNow": "Poskusi zdaj", // 商务会议助手页 "startMeeting": "Začni sestanek", @@ -213,8 +201,12 @@ const Map slSI = { 'meetingTranscript': 'Prepis sestanka', // 设置页 - "systemSettings": "Sistemske nastavitve", - "premiumMember": "Premium član", + "systemSettings": "Sistemske nastavitve", //系统设置 + "vipRecharge": "Nákup premium člana", //会员充值 + "aiRecharge": "Premium član", //AI充值 + "translationRecharge": "Nákup prevajanja", //翻译充值 + "meetingAssistantRecharge": "Nákup asistenta sestanka", //会议助手充值 + "cloudSync": "Nákup sinkronizacije v oblaku", //云端同步 "joinDevice": "Povezana naprava:", "accountId": "ID računa:", "systemSetting": "Sistemska nastavitev", @@ -284,7 +276,7 @@ const Map slSI = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Počisti zgodovino klepeta", "online": "Na spletu", "inputMessage": "Vnesite sporočilo...", @@ -311,7 +303,7 @@ const Map slSI = { "swipeUpForMore": "Povlecite navzgor za več", // Agent controller - 'welcomeMessage': 'Živjo! Sem **XiaoYan**, lahko vam pomagam:\n\n' + 'welcomeMessage': 'Živjo! Sem **Lily**, lahko vam pomagam:\n\n' '• Odgovarjam na vprašanja in nudim informacije\n\n' '• Glasovno prevajanje v realnem času\n\n' '• Priporočam glasbo in podcaste\n\n' @@ -704,7 +696,7 @@ const Map slSI = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Seznanjanje naprave", - "connectYourDeepSoundHeadset": "Povežite svoje slušalke DeepSound", + "connectYourDeepSoundHeadset": "Povežite svoje slušalke Voitrans", "bind": "Poveži", "authorizeBind": "Avtoriziraj povezavo", "enterCode": "Vnesite kodo", @@ -759,7 +751,7 @@ const Map slSI = { 'retryAuthorization': 'Ponovno avtoriziraj', 'pleaseGrantNecessaryPermissions': 'Podelite potrebna dovoljenja', 'checkFailedRetry': 'Preverjanje ni uspelo, poskusite znova', - 'pleaseConnectDeepSoundDevice': 'Povežite napravo DeepSound', + 'pleaseConnectDeepSoundDevice': 'Povežite napravo Voitrans', 'verificationFailedRetry': 'Preverjanje ni uspelo, poskusite znova', 'invalidDeviceAddress': 'Neveljaven naslov naprave', 'bleDeviceAssociationFailed': 'Povezovanje naprave BLE ni uspelo', diff --git a/lib/core/translations/language/sr_rs.dart b/lib/core/translations/language/sr_rs.dart index 7bab2c689..6dd585a23 100644 --- a/lib/core/translations/language/sr_rs.dart +++ b/lib/core/translations/language/sr_rs.dart @@ -136,7 +136,8 @@ const Map srRS = { "bluetoothInitializationFailure": "Neuspešna inicijalizacija Bluetooth servisa", "checkingPermissions": "Provera dozvola...", - "lackPermissions": "Nedostaju Bluetooth dozvole", + "lackPermissions": + "Nedostaju Bluetooth dozvole i je potrebno omogućiti pristup lokaciji", "searchingDevice": "Pretraga uređaja...", "findConnectedDevice": "Pronađen povezan uređaj", "notFound": "Nije pronađen povezan Bluetooth uređaj", @@ -181,22 +182,9 @@ const Map srRS = { "conferenceAssistant": "Asistent za poslovne sastanke", "conferenceAssistantInfo": "Beleženje zapisnika u realnom vremenu, višejezično prevođenje", - "studyAssistant": "Asistent za učenje jezika", - "studyAssistantInfo": "Korekcija izgovora, vežbanje govora u realnom vremenu", - "travelTranslator": "Asistent za prevođenje na putovanjima", - "travelTranslatorInfo": - "Glasovno prevođenje u realnom vremenu, predstavljanje turističkih atrakcija", - "beSearching": "Pretraga:", - "noAssistantFound": "Nije pronađen odgovarajući AI asistent", + "usageStats": "Statistika korišćenja", + "usageStatsInfo": "Pregledajte vaše korišćenje aplikacije", "scene": "Scena", - "youHaveSelected": "Izabrali ste:", - "newFunction": "Nova funkcija", - "youChoseTry": "Izabrali ste da probate", - "aiSinger": "AI pevač mod", - "aiSingerInfo": - "Pevajte bilo koju pesmu glasom svog omiljenog pevača, čak i sopstvene kompozicije", - "popularFunction": "Popularne scene funkcija", - "tryNow": "Probaj sada", // 商务会议助手页 "startMeeting": "Započni sastanak", @@ -212,8 +200,12 @@ const Map srRS = { 'meetingTranscript': 'Transkript sastanka', // 设置页 - "systemSettings": "Sistemska podešavanja", - "premiumMember": "Premium član", + "systemSettings": "Sistemska podešavanja", //系统设置 + "vipRecharge": "Nákup premium člana", //会员充值 + "aiRecharge": "Premium član", //AI充值 + "translationRecharge": "Nákup prevajanja", //翻译充值 + "meetingAssistantRecharge": "Nákup asistenta sastanka", //会议助手充值 + "cloudSync": "Nákup sinkronizacije u oblaku", //云端同步 "joinDevice": "Povezan uređaj:", "accountId": "ID naloga:", "systemSetting": "Sistemsko podešavanje", @@ -282,7 +274,7 @@ const Map srRS = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Obriši istoriju ćaskanja", "online": "Na mreži", "inputMessage": "Unesite poruku...", @@ -309,7 +301,7 @@ const Map srRS = { "swipeUpForMore": "Prevucite nagore za više", // Agent controller - 'welcomeMessage': 'Zdravo! Ja sam **XiaoYan**, mogu vam pomoći:\n\n' + 'welcomeMessage': 'Zdravo! Ja sam **Lily**, mogu vam pomoći:\n\n' '• Da odgovaram na pitanja i pružam informacije\n\n' '• Glasovno prevođenje u realnom vremenu\n\n' '• Da preporučujem muziku i podkaste\n\n' @@ -699,7 +691,7 @@ const Map srRS = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Uparivanje uređaja", - "connectYourDeepSoundHeadset": "Povežite svoje DeepSound slušalice", + "connectYourDeepSoundHeadset": "Povežite svoje Voitrans slušalice", "bind": "Poveži", "authorizeBind": "Autorizuj povezivanje", "enterCode": "Unesite kod", @@ -754,7 +746,7 @@ const Map srRS = { 'retryAuthorization': 'Pokušaj ponovo autorizaciju', 'pleaseGrantNecessaryPermissions': 'Dajte potrebne dozvole', 'checkFailedRetry': 'Neuspešna provera, pokušajte ponovo', - 'pleaseConnectDeepSoundDevice': 'Povežite DeepSound uređaj', + 'pleaseConnectDeepSoundDevice': 'Povežite Voitrans uređaj', 'verificationFailedRetry': 'Neuspešna verifikacija, pokušajte ponovo', 'invalidDeviceAddress': 'Nevažeća adresa uređaja', 'bleDeviceAssociationFailed': 'Neuspešno povezivanje BLE uređaja', diff --git a/lib/core/translations/language/sv_se.dart b/lib/core/translations/language/sv_se.dart index d307b1eef..a44d13fe2 100644 --- a/lib/core/translations/language/sv_se.dart +++ b/lib/core/translations/language/sv_se.dart @@ -136,7 +136,8 @@ const Map svSE = { "bluetoothInitializationFailure": "Initiering av Bluetooth-tjänst misslyckades", "checkingPermissions": "Kontrollerar behörigheter...", - "lackPermissions": "Saknar Bluetooth-behörigheter", + "lackPermissions": + "Saknar Bluetooth-behörigheter och kräver att du tillåter åtkomst till plats", "searchingDevice": "Söker efter enheter...", "findConnectedDevice": "Hittade ansluten enhet", "notFound": "Ingen ansluten Bluetooth-enhet hittades", @@ -181,23 +182,9 @@ const Map svSE = { "conferenceAssistant": "Affärsmötesassistent", "conferenceAssistantInfo": "Realtidsinspelning av mötesprotokoll, flerspråkig översättning", - "studyAssistant": "Språkinlärningsassistent", - "studyAssistantInfo": - "Uttalskorrigering, realtidsövning av muntlig färdighet", - "travelTranslator": "Reseöversättningsassistent", - "travelTranslatorInfo": - "Realtids röstöversättning, introduktion till turistattraktioner", - "beSearching": "Söker:", - "noAssistantFound": "Ingen motsvarande AI-assistent hittades", + "usageStats": "Användningsstatistik", + "usageStatsInfo": "Visa din användningsstatistik", "scene": "Scenario", - "youHaveSelected": "Du har valt:", - "newFunction": "Ny funktion", - "youChoseTry": "Du valde att prova", - "aiSinger": "AI-sångarläge", - "aiSingerInfo": - "Sjung vilken låt som helst med din favoritsångares röst, även dina egna kompositioner", - "popularFunction": "Populära funktionsscenarier", - "tryNow": "Prova nu", // 商务会议助手页 "startMeeting": "Starta möte", @@ -213,8 +200,12 @@ const Map svSE = { 'meetingTranscript': 'Mötesutskrift', // 设置页 - "systemSettings": "Systeminställningar", - "premiumMember": "Premium-medlem", + "systemSettings": "Systeminställningar", //系统设置 + "vipRecharge": "Premium-låntagning", //会员充值 + "aiRecharge": "Premium-medlem", //AI充值 + "translationRecharge": "Översättningslåntagning", //翻译充值 + "meetingAssistantRecharge": "Mötesassistentlåntagning", //会议助手充值 + "cloudSync": "Molnsynkronisering", //云端同步 "joinDevice": "Ansluten enhet:", "accountId": "Konto-ID:", "systemSetting": "Systeminställning", @@ -283,7 +274,7 @@ const Map svSE = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Rensa chatthistorik", "online": "Online", "inputMessage": "Skriv meddelande...", @@ -310,7 +301,7 @@ const Map svSE = { "swipeUpForMore": "Svep upp för mer", // Agent controller - 'welcomeMessage': 'Hej! Jag är **XiaoYan**, jag kan hjälpa dig med:\n\n' + 'welcomeMessage': 'Hej! Jag är **Lily**, jag kan hjälpa dig med:\n\n' '• Svara på frågor och ge information\n\n' '• Realtids röstöversättning\n\n' '• Rekommendera musik och poddar\n\n' @@ -699,7 +690,7 @@ const Map svSE = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Enhetsparkoppling", - "connectYourDeepSoundHeadset": "Anslut dina DeepSound-hörlurar", + "connectYourDeepSoundHeadset": "Anslut dina Voitrans-hörlurar", "bind": "Bind", "authorizeBind": "Auktorisera bindning", "enterCode": "Ange koden", @@ -753,7 +744,7 @@ const Map svSE = { 'retryAuthorization': 'Försök auktorisera igen', 'pleaseGrantNecessaryPermissions': 'Bevilja nödvändiga behörigheter', 'checkFailedRetry': 'Kontroll misslyckades, försök igen', - 'pleaseConnectDeepSoundDevice': 'Anslut en DeepSound-enhet', + 'pleaseConnectDeepSoundDevice': 'Anslut en Voitrans-enhet', 'verificationFailedRetry': 'Verifiering misslyckades, försök igen', 'invalidDeviceAddress': 'Ogiltig enhetsadress', 'bleDeviceAssociationFailed': 'BLE-enhetsassociation misslyckades', diff --git a/lib/core/translations/language/th_th.dart b/lib/core/translations/language/th_th.dart index 084def1f8..3f828f4f6 100644 --- a/lib/core/translations/language/th_th.dart +++ b/lib/core/translations/language/th_th.dart @@ -132,7 +132,7 @@ const Map thTH = { "initializing": "กำลังเริ่มต้น...", "bluetoothInitializationFailure": "การเริ่มต้นบริการบลูทูธล้มเหลว", "checkingPermissions": "กำลังตรวจสอบสิทธิ์...", - "lackPermissions": "ขาดสิทธิ์บลูทูธ", + "lackPermissions": "ขาดสิทธิ์บลูทูธและต้องอนุญาตการเข้าถึงตำแหน่ง", "searchingDevice": "กำลังค้นหาอุปกรณ์...", "findConnectedDevice": "พบอุปกรณ์ที่เชื่อมต่อแล้ว", "notFound": "ไม่พบอุปกรณ์บลูทูธที่เชื่อมต่อ", @@ -176,21 +176,9 @@ const Map thTH = { "healthConsultantInfo": "มีฟังก์ชันแชท AI เพิ่มเติม", "conferenceAssistant": "ผู้ช่วยประชุมธุรกิจ", "conferenceAssistantInfo": "บันทึกการประชุมแบบเรียลไทม์ แปลหลายภาษา", - "studyAssistant": "ผู้ช่วยเรียนภาษา", - "studyAssistantInfo": "แก้ไขการออกเสียง ฝึกพูดแบบเรียลไทม์", - "travelTranslator": "ผู้ช่วยแปลภาษาสำหรับการเดินทาง", - "travelTranslatorInfo": "แปลเสียงแบบเรียลไทม์ แนะนำสถานที่ท่องเที่ยว", - "beSearching": "กำลังค้นหา:", - "noAssistantFound": "ไม่พบผู้ช่วย AI ที่ตรงกัน", + "usageStats": "สถิติการใช้งาน", + "usageStatsInfo": "ดูสถิติการใช้งานของคุณ", "scene": "สถานการณ์", - "youHaveSelected": "คุณได้เลือก:", - "newFunction": "ฟังก์ชันใหม่", - "youChoseTry": "คุณเลือกที่จะลอง", - "aiSinger": "โหมดนักร้อง AI", - "aiSingerInfo": - "ร้องเพลงใดก็ได้ด้วยเสียงของนักร้องที่คุณชื่นชอบ แม้แต่เพลงที่คุณแต่งเอง", - "popularFunction": "สถานการณ์ฟังก์ชันยอดนิยม", - "tryNow": "ลองเลย", // 商务会议助手页 "startMeeting": "เริ่มการประชุม", @@ -206,8 +194,12 @@ const Map thTH = { 'meetingTranscript': 'ข้อความถอดเสียงการประชุม', // 设置页 - "systemSettings": "การตั้งค่าระบบ", - "premiumMember": "สมาชิกระดับพรีเมียม", + "systemSettings": "การตั้งค่าระบบ", //系统设置 + "vipRecharge": "การเติมเงินสมาชิกระดับพรีเมียม", //会员充值 + "aiRecharge": "สมาชิกระดับพรีเมียม", //AI充值 + "translationRecharge": "การเติมเงินการแปล", //翻译充值 + "meetingAssistantRecharge": "การเติมเงินผู้ช่วยการประชุม", //会议助手充值 + "cloudSync": "การเติมเงินการ đồng bộ hóaคลาวด์", //云端同步 "joinDevice": "อุปกรณ์ที่เชื่อมต่อ:", "accountId": "ID บัญชี:", "systemSetting": "การตั้งค่าระบบ", @@ -276,7 +268,7 @@ const Map thTH = { //Agent 模块 // agent View - "XiaoYan": "เสี่ยวเหยียน", + "Lily": "Lily", "clearChatHistory": "ล้างประวัติการแชท", "online": "ออนไลน์", "inputMessage": "ป้อนข้อความ...", @@ -685,7 +677,7 @@ const Map thTH = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "การจับคู่อุปกรณ์", - "connectYourDeepSoundHeadset": "เชื่อมต่อหูฟัง DeepSound ของคุณ", + "connectYourDeepSoundHeadset": "เชื่อมต่อหูฟัง Voitrans ของคุณ", "bind": "ผูก", "authorizeBind": "อนุญาตการผูก", "enterCode": "กรุณาใส่รหัส", diff --git a/lib/core/translations/language/tl_ph.dart b/lib/core/translations/language/tl_ph.dart index 85d3609fd..ee9c6d87d 100644 --- a/lib/core/translations/language/tl_ph.dart +++ b/lib/core/translations/language/tl_ph.dart @@ -138,7 +138,8 @@ const Map tlPH = { "bluetoothInitializationFailure": "Nabigo ang pag-initialize ng serbisyo ng Bluetooth", "checkingPermissions": "Sinusuri ang mga pahintulot...", - "lackPermissions": "Kulang sa mga pahintulot ng Bluetooth", + "lackPermissions": + "Kulang sa mga pahintulot ng Bluetooth at sumasang-ayon ka sa lokasyon", "searchingDevice": "Naghahanap ng mga device...", "findConnectedDevice": "Natagpuan ang konektadong device", "notFound": "Walang natagpuang konektadong Bluetooth device", @@ -184,23 +185,9 @@ const Map tlPH = { "conferenceAssistant": "Business Meeting Assistant", "conferenceAssistantInfo": "Real-time na pagtatala ng mga minuto ng pulong, pagsasalin sa maraming wika", - "studyAssistant": "Language Learning Assistant", - "studyAssistantInfo": - "Pagwawasto ng bigkas, real-time na pagsasanay sa pagsasalita", - "travelTranslator": "Travel Translation Assistant", - "travelTranslatorInfo": - "Real-time na pagsasalin ng boses, pagpapakilala sa mga pasyalan", - "beSearching": "Naghahanap:", - "noAssistantFound": "Walang natagpuang katugmang AI assistant", + "usageStats": "Usage Statistics", + "usageStatsInfo": "View your usage statistics", "scene": "Eksena", - "youHaveSelected": "Pinili mo ang:", - "newFunction": "Bagong Function", - "youChoseTry": "Pinili mong subukan", - "aiSinger": "AI Singer Mode", - "aiSingerInfo": - "Kantahin ang anumang kanta sa boses ng paborito mong mang-aawit, kahit na ang sarili mong mga likha", - "popularFunction": "Mga Sikat na Eksena ng Function", - "tryNow": "Subukan Ngayon", // 商务会议助手页 "startMeeting": "Simulan ang Pulong", @@ -216,8 +203,12 @@ const Map tlPH = { 'meetingTranscript': 'Transkripsyon ng Pulong', // 设置页 - "systemSettings": "Mga Setting ng System", - "premiumMember": "Premium na Miyembro", + "systemSettings": "Mga Setting ng System", //系统设置 + "vipRecharge": "Pagsasalin ng Recharge ng Premium na Miyembro", //会员充值 + "aiRecharge": "Premium na Miyembro", //AI充值 + "translationRecharge": "Pagsasalin ng Recharge", //翻译充值 + "meetingAssistantRecharge": "Pagsasalin ng Recharge ng Pulong", //会议助手充值 + "cloudSync": "Pagsasalin ng Recharge ng Cloud", //云端同步 "joinDevice": "Konektadong device:", "accountId": "Account ID:", "systemSetting": "Setting ng System", @@ -289,7 +280,7 @@ const Map tlPH = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Burahin ang Kasaysayan ng Chat", "online": "Online", "inputMessage": "Mag-type ng mensahe...", @@ -317,7 +308,7 @@ const Map tlPH = { "swipeUpForMore": "Mag-swipe pataas para sa higit pa", // Agent controller - 'welcomeMessage': 'Kumusta! Ako si **XiaoYan**, matutulungan kita sa:\n\n' + 'welcomeMessage': 'Kumusta! Ako si **Lily**, matutulungan kita sa:\n\n' '• Pagsagot sa mga tanong at pagbibigay ng impormasyon\n\n' '• Real-time na pagsasalin ng boses\n\n' '• Pagrekomenda ng musika at mga podcast\n\n' @@ -712,7 +703,7 @@ const Map tlPH = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Pag-pair ng Device", - "connectYourDeepSoundHeadset": "Ikonekta ang iyong DeepSound Headset", + "connectYourDeepSoundHeadset": "Ikonekta ang iyong Voitrans Headset", "bind": "I-bind", "authorizeBind": "Pahintulutan ang pag-bind", "enterCode": "Pakilagay ang code", diff --git a/lib/core/translations/language/tr_tr.dart b/lib/core/translations/language/tr_tr.dart index a99131602..c981bdeba 100644 --- a/lib/core/translations/language/tr_tr.dart +++ b/lib/core/translations/language/tr_tr.dart @@ -135,7 +135,7 @@ const Map trTR = { "initializing": "Başlatılıyor...", "bluetoothInitializationFailure": "Bluetooth hizmeti başlatılamadı", "checkingPermissions": "İzinler kontrol ediliyor...", - "lackPermissions": "Bluetooth izinleri eksik", + "lackPermissions": "Bluetooth izinleri eksik ve lokasyon erişimi gereklidir", "searchingDevice": "Cihaz aranıyor...", "findConnectedDevice": "Bağlı cihaz bulundu", "notFound": "Bağlı Bluetooth cihazı bulunamadı", @@ -180,22 +180,9 @@ const Map trTR = { "conferenceAssistant": "İş Toplantısı Asistanı", "conferenceAssistantInfo": "Gerçek zamanlı toplantı tutanakları, çok dilli çeviri", - "studyAssistant": "Dil Öğrenme Asistanı", - "studyAssistantInfo": "Telaffuz düzeltme, gerçek zamanlı konuşma pratiği", - "travelTranslator": "Seyahat Çeviri Asistanı", - "travelTranslatorInfo": - "Gerçek zamanlı sesli çeviri, turistik yer tanıtımları", - "beSearching": "Aranıyor:", - "noAssistantFound": "İlgili yapay zeka asistanı bulunamadı", + "usageStats": "Kullanım İstatistikleri", + "usageStatsInfo": "Kullanım istatistiklerinizi görüntüleyin", "scene": "Senaryo", - "youHaveSelected": "Seçtiniz:", - "newFunction": "Yeni Özellik", - "youChoseTry": "Denemeyi seçtiniz", - "aiSinger": "Yapay Zeka Şarkıcı Modu", - "aiSingerInfo": - "En sevdiğiniz sanatçının sesiyle herhangi bir şarkıyı, hatta kendi bestelerinizi söyleyin", - "popularFunction": "Popüler Özellik Senaryoları", - "tryNow": "Şimdi Dene", // 商务会议助手页 "startMeeting": "Toplantıyı Başlat", @@ -211,8 +198,12 @@ const Map trTR = { 'meetingTranscript': 'Toplantı Metni', // 设置页 - "systemSettings": "Sistem Ayarları", - "premiumMember": "Premium Üye", + "systemSettings": "Sistem Ayarları", //系统设置 + "vipRecharge": "Premium Üye Recharge", //会员充值 + "aiRecharge": "Premium Üye", //AI充值 + "translationRecharge": "Çeviri Toplantı Recharge", //翻译充值 + "meetingAssistantRecharge": "Toplantı Asistanı Recharge", //会议助手充值 + "cloudSync": "Bulut Sincronizasyonu", //云端同步 "joinDevice": "Bağlı cihaz:", "accountId": "Hesap Kimliği:", "systemSetting": "Sistem Ayarları", @@ -283,7 +274,7 @@ const Map trTR = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Sohbet Geçmişini Temizle", "online": "Çevrimiçi", "inputMessage": "Mesaj girin...", @@ -311,7 +302,7 @@ const Map trTR = { // Agent controller 'welcomeMessage': - 'Merhaba! Ben **XiaoYan**, size şu konularda yardımcı olabilirim:\n\n' + 'Merhaba! Ben **Lily**, size şu konularda yardımcı olabilirim:\n\n' '• Soruları yanıtlamak ve bilgi sağlamak\n\n' '• Gerçek zamanlı sesli çeviri\n\n' '• Müzik ve podcast önermek\n\n' @@ -750,7 +741,7 @@ const Map trTR = { 'retryAuthorization': 'Yetkilendirmeyi Yeniden Dene', 'pleaseGrantNecessaryPermissions': 'Lütfen gerekli izinleri verin', 'checkFailedRetry': 'Kontrol başarısız, lütfen tekrar deneyin', - 'pleaseConnectDeepSoundDevice': 'Lütfen bir DeepSound cihazı bağlayın', + 'pleaseConnectDeepSoundDevice': 'Lütfen bir Voitrans cihazı bağlayın', 'verificationFailedRetry': 'Doğrulama başarısız, lütfen tekrar deneyin', 'invalidDeviceAddress': 'Geçersiz cihaz adresi', 'bleDeviceAssociationFailed': 'BLE cihaz ilişkisi kurulamadı', diff --git a/lib/core/translations/language/uk_ua.dart b/lib/core/translations/language/uk_ua.dart index 1dabffdb0..fd1d08b77 100644 --- a/lib/core/translations/language/uk_ua.dart +++ b/lib/core/translations/language/uk_ua.dart @@ -137,7 +137,8 @@ const Map ukUA = { "initializing": "Ініціалізація...", "bluetoothInitializationFailure": "Помилка ініціалізації служби Bluetooth", "checkingPermissions": "Перевірка дозволів...", - "lackPermissions": "Відсутні дозволи Bluetooth", + "lackPermissions": + "Відсутні дозволи Bluetooth і необхідно дозволити доступ до місцезнаходження", "searchingDevice": "Пошук пристроїв...", "findConnectedDevice": "Знайдено підключений пристрій", "notFound": "Не знайдено підключеного пристрою Bluetooth", @@ -182,23 +183,9 @@ const Map ukUA = { "conferenceAssistant": "Помічник для ділових зустрічей", "conferenceAssistantInfo": "Запис протоколів у реальному часі, багатомовний переклад", - "studyAssistant": "Помічник у вивченні мов", - "studyAssistantInfo": - "Виправлення вимови, практика розмовної мови в реальному часі", - "travelTranslator": "Помічник-перекладач для подорожей", - "travelTranslatorInfo": - "Голосовий переклад у реальному часі, опис туристичних місць", - "beSearching": "Пошук:", - "noAssistantFound": "Не знайдено відповідного помічника AI", + "usageStats": "Статистика використання", + "usageStatsInfo": "Перегляньте статистику використання вашого пристрою", "scene": "Сценарій", - "youHaveSelected": "Ви обрали:", - "newFunction": "Нова функція", - "youChoseTry": "Ви вирішили спробувати", - "aiSinger": "Режим AI співака", - "aiSingerInfo": - "Співайте будь-яку пісню голосом улюбленого співака, навіть пісні власного авторства", - "popularFunction": "Популярні функціональні сценарії", - "tryNow": "Спробувати зараз", // 商务会议助手页 "startMeeting": "Почати зустріч", @@ -214,8 +201,12 @@ const Map ukUA = { 'meetingTranscript': 'Транскрипт зустрічі', // 设置页 - "systemSettings": "Системні налаштування", - "premiumMember": "Преміум-учасник", + "systemSettings": "Системні налаштування", //系统设置 + "vipRecharge": "Підписатися на премиум-членство", //会员充值 + "aiRecharge": "Преміум-учасник", //AI充值 + "translationRecharge": "Підписатися на переклад", //翻译充值 + "meetingAssistantRecharge": "Підписатися на допомогу з зустрічю", //会议助手充值 + "cloudSync": "Увімкнути синхронізацію в хмарі", //云端同步 "joinDevice": "Підключений пристрій:", "accountId": "ID облікового запису:", "systemSetting": "Системні налаштування", @@ -284,7 +275,7 @@ const Map ukUA = { //Agent 模块 // agent View - "XiaoYan": "СяоЯнь", + "Lily": "Lily", "clearChatHistory": "Очистити історію чату", "online": "В мережі", "inputMessage": "Введіть повідомлення...", diff --git a/lib/core/translations/language/vi_vn.dart b/lib/core/translations/language/vi_vn.dart index fc422ee38..bc96afa82 100644 --- a/lib/core/translations/language/vi_vn.dart +++ b/lib/core/translations/language/vi_vn.dart @@ -133,7 +133,8 @@ const Map viVN = { "initializing": "Đang khởi tạo...", "bluetoothInitializationFailure": "Khởi tạo dịch vụ Bluetooth thất bại", "checkingPermissions": "Đang kiểm tra quyền...", - "lackPermissions": "Thiếu quyền Bluetooth", + "lackPermissions": + "Thiếu quyền Bluetooth và cần phải cấp quyền truy cập vị trí", "searchingDevice": "Đang tìm kiếm thiết bị...", "findConnectedDevice": "Đã tìm thấy thiết bị đã kết nối", "notFound": "Không tìm thấy thiết bị Bluetooth đã kết nối", @@ -178,22 +179,9 @@ const Map viVN = { "conferenceAssistant": "Trợ lý hội nghị kinh doanh", "conferenceAssistantInfo": "Ghi lại biên bản họp thời gian thực, dịch đa ngôn ngữ", - "studyAssistant": "Trợ lý học ngôn ngữ", - "studyAssistantInfo": "Sửa lỗi phát âm, luyện nói thời gian thực", - "travelTranslator": "Trợ lý dịch thuật du lịch", - "travelTranslatorInfo": - "Dịch giọng nói thời gian thực, giới thiệu các điểm du lịch", - "beSearching": "Đang tìm kiếm:", - "noAssistantFound": "Không tìm thấy trợ lý AI tương ứng", + "usageStats": "Thống kê sử dụng", + "usageStatsInfo": "Xem thống kê sử dụng của bạn", "scene": "Tình huống", - "youHaveSelected": "Bạn đã chọn:", - "newFunction": "Chức năng mới", - "youChoseTry": "Bạn đã chọn thử", - "aiSinger": "Chế độ ca sĩ AI", - "aiSingerInfo": - "Hát bất kỳ bài hát nào bằng giọng của ca sĩ bạn yêu thích, thậm chí cả bài hát do chính bạn sáng tác", - "popularFunction": "Các tình huống chức năng phổ biến", - "tryNow": "Thử ngay", // 商务会议助手页 "startMeeting": "Bắt đầu cuộc họp", @@ -209,8 +197,12 @@ const Map viVN = { 'meetingTranscript': 'Bản ghi cuộc họp', // 设置页 - "systemSettings": "Cài đặt hệ thống", - "premiumMember": "Thành viên Premium", + "systemSettings": "Cài đặt hệ thống", //系统设置 + "vipRecharge": "Nạp lại dịch vụ Premium", //会员充值 + "aiRecharge": "Thành viên Premium", //AI充值 + "translationRecharge": "Nạp lại dịch vụ dịch", //翻译充值 + "meetingAssistantRecharge": "Nạp lại dịch vụ hỗ trợ họp", //会议助手充值 + "cloudSync": "Chuyển đổi đồng bộ hóa đám mây", //云端同步 "joinDevice": "Thiết bị đã kết nối:", "accountId": "ID tài khoản:", "systemSetting": "Cài đặt hệ thống", @@ -279,7 +271,7 @@ const Map viVN = { //Agent 模块 // agent View - "XiaoYan": "XiaoYan", + "Lily": "Lily", "clearChatHistory": "Xóa lịch sử trò chuyện", "online": "Trực tuyến", "inputMessage": "Nhập tin nhắn...", @@ -306,7 +298,7 @@ const Map viVN = { "swipeUpForMore": "Vuốt lên để xem thêm", // Agent controller - 'welcomeMessage': 'Chào bạn! Tôi là **XiaoYan**, tôi có thể giúp bạn:\n\n' + 'welcomeMessage': 'Chào bạn! Tôi là **Lily**, tôi có thể giúp bạn:\n\n' '• Trả lời câu hỏi và cung cấp thông tin\n\n' '• Dịch giọng nói thời gian thực\n\n' '• Đề xuất nhạc và podcast\n\n' @@ -692,7 +684,7 @@ const Map viVN = { // 配对模块 // 配对页面 (pairing_view.dart & pairing_view_new.dart) "devicePairing": "Ghép nối thiết bị", - "connectYourDeepSoundHeadset": "Kết nối tai nghe DeepSound của bạn", + "connectYourDeepSoundHeadset": "Kết nối tai nghe Voitrans của bạn", "bind": "Liên kết", "authorizeBind": "Ủy quyền liên kết", "enterCode": "Vui lòng nhập mã", diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index 74d2c9999..fd1602857 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -132,7 +132,7 @@ const Map zhCN = { "initializing": "正在初始化...", "bluetoothInitializationFailure": "蓝牙服务初始化失败", "checkingPermissions": "正在检查权限...", - "lackPermissions": "缺少蓝牙权限", + "lackPermissions": "开启蓝牙和位置权限,才能发现并连接附近的设备", "searchingDevice": "正在搜索设备...", "findConnectedDevice": "找到已连接的设备", "notFound": "未找到已连接的蓝牙设备", @@ -162,7 +162,7 @@ const Map zhCN = { "exploreSlogan1": "智能陪伴,无限可能", "exploreSlogan2": "智能眼镜,无限可能", "exploreAi": "探索Voitrans提供的AI功能", - "aiFunction": "AI功能", + "aiFunction": "AI助手", "allRoundAssistant": "全能助理", "allRoundAssistantInfo": "帮你解决各类问题", "simultaneousTranslation": "同声翻译", @@ -176,20 +176,197 @@ const Map zhCN = { "healthConsultantInfo": "提供更多AI聊天功能", "conferenceAssistant": "商务会议助手", "conferenceAssistantInfo": "实时记录会议纪要,多语言翻译", - "studyAssistant": "语言学习助手", - "studyAssistantInfo": "发音纠正,实时练习口语", - "travelTranslator": "旅行翻译助手", - "travelTranslatorInfo": "实时语音翻译,旅游景点介绍", - "beSearching": "正在搜索:", - "noAssistantFound": "未找到对应的AI助手", + "usageStats": "用量统计", + "usageStatsInfo": "查看您的使用统计数据", "scene": "场景", - "youHaveSelected": "您选择了:", - "newFunction": "新功能", - "youChoseTry": "您选择了尝试", - "aiSinger": "AI歌手模式", - "aiSingerInfo": "用你喜欢的歌手声音演唱任何歌曲,甚至是你自己创作的歌曲", - "popularFunction": "热门功能场景", - "tryNow": "立即尝试", + // 用量统计模块 + "aiChat": "AI聊天", + "translationStats": "翻译统计", + "meetingStats": "会议统计", + "today": "今日", + "thisWeek": "本周", + "thisMonth": "本月", + "history": "历史", + "currentSessionStats": "当前会话统计", + "translationCount": "翻译次数", + "sessionDuration": "会话时长", + "characters": "字符数", + "recordingDuration": "录音时长", + "totalApiCalls": "总调用次数", + "sessionCount": "会话数量", + "totalCharacters": "总字符数", + "apiCallStats": "API调用统计", + "translationApi": "翻译API", + "asr": "语音识别", + "tts": "语音合成", + //"aiSummary": "AI摘要", + "dataVolumeStats": "数据量统计", + "sourceTextCharacters": "源文本字符", + "ttsTextCharacters": "TTS文本字符", + "effectiveAudioDuration": "有效音频时长", + "totalSessionDuration": "总会话时长", + "costStats": "成本统计", + "estimatedCost": "预估成本", + "dailyCostThreshold": "日成本预警阈值", + "monthlyCostThreshold": "月成本预警阈值", + "usageBehaviorStats": "使用行为统计", + "mostUsedLanguagePair": "最常用语言对", + "mostUsedMode": "最常用模式", + "asrApiCalls": "ASR调用次数", + "totalAudioDuration": "音频总时长", + "audioFileSize": "音频文件大小", + "transcriptBytes": "转写字节", + // 通知文案 + "loadFailed": "加载失败", + "statsLoadFailedTryLater": "统计数据加载失败,请稍后重试", + "cleanupComplete": "清理完成", + "cleanedOldData": "已清理90天前的过期数据", + "exportFeature": "导出功能", + "exportInDevelopment": "数据导出功能正在开发中", + + // 商品页面(AI/翻译/会议) + 'availablePackages': '可购买套餐', + 'remainingQuota': '剩余额度', + 'totalQuota': '总额度', + 'remaining': '剩余', + 'featureDescription': '功能说明', + // AI 功能卡片 + 'musicAndRecommendation': '听歌与推荐', + 'navigationAndRoutes': '导航与路线', + 'nearbySearch': '附近搜索', + 'newsInformation': '新闻资讯', + 'communicationAssistant': '通讯助手', + // 翻译功能卡片 + 'realtimeTranscription': '实时同传', + // 'textTranslation' 已存在 + 'sceneLexicon': '场景词库', + 'subtitleDisplay': '字幕显示', + // 会议功能卡片 + 'meetingRecording': '会议记录', + 'transcriptionAndSummary': '转写与总结', + 'keywordSearch': '关键词检索', + 'collaborationSharing': '协同分享', + // 商品名称(按ID) + 'ai_001_name': 'AI基础 100 次', + 'ai_002_name': 'AI基础 500 次', + 'ai_003_name': 'AI基础 1000 次', + 'meet_001_name': '会议基础 100 次', + 'meet_002_name': '会议基础 500 次', + 'meet_003_name': '会议基础 1000 次', + 'trans_001_name': '翻译基础 100 次', + 'trans_002_name': '翻译基础 500 次', + 'trans_003_name': '翻译基础 1000 次', + + // 商品描述(按ID) + 'ai_001_desc': '含 100 次使用,覆盖核心 AI 功能', + 'ai_002_desc': '含 500 次使用,覆盖核心 AI 功能', + 'ai_003_desc': '含 1000 次使用,覆盖核心 AI 功能', + 'meet_001_desc': '含 100 次会议录音/转写', + 'meet_002_desc': '含 500 次会议录音/转写', + 'meet_003_desc': '含 1000 次会议录音/转写', + 'trans_001_desc': '含 100 次翻译(语音/文本)', + 'trans_002_desc': '含 500 次翻译(语音/文本)', + 'trans_003_desc': '含 1000 次翻译(语音/文本)', + +// 商品页通用页脚 + 'appNameProvider': '应用名称:Voitrans · 服务提供:Voitrans', + 'iapCustomerServiceQQLabel': '内购商品专属客服QQ:@qq', + 'copyrightVoitrans': '© 2025 Voitrans Technologies', + + // 会议商品页描述 + 'meetingHeaderDesc1': '录音转写、关键词检索与自动总结,提升会议效率。', + 'meetingHeaderDesc2': '适用于商务会议、课堂笔记、访谈纪要等场景。', + 'meetingAutoRecording': '自动录音与分段', + 'meetingSpeakerTagging': '讲话人标注', + 'meetingLongAudioTranscription': '长音频转写', + 'meetingKeyPointSummary': '关键要点提炼', + 'meetingTopicNameSearch': '按主题/人名检索', + 'meetingTimelineJump': '时间线跳转', + 'meetingExportSummary': '导出纪要', + 'meetingMultiPlatformShare': '多平台分享', + 'meetingDisclaimer': '建议在安静环境使用,效果受麦克风与网络影响,具体以实际表现为准。', + + // 同传/翻译商品页描述 + 'transHeaderDesc1': '多语种实时同传与文本翻译,低延时语音播报。', + 'transHeaderDesc2': '适用于出行、商务、演讲与教学等场景。', + 'translateZhEn': '中英互译', + 'lowLatencyVoiceBroadcast': '低延时语音播报', + 'multiLangTextTranslation': '多语种文本翻译', + 'termOptimizationContextPreserve': '术语优化与语境保持', + 'lexiconTourBusinessLife': '旅游/商务/生活词库', + 'personalVocabularyMemory': '专属词汇记忆', + 'conversationSubtitleLargeText': '对话字幕/大字模式', + 'colorHighContrast': '颜色高对比度', + 'transDisclaimer': '翻译效果受麦克风、网络时延与环境噪声影响,具体以实际表现为准。', + + // AI 商品页描述 + 'aiHeaderDesc1': '更长对话额度、更多工具调用、更强上下文长度支持。', + 'aiHeaderDesc2': '适用于智能体问答、总结分析、任务执行等场景。', + 'musicSearchPlaybackControl': '检索曲库、播放控制', + 'smartPlaylistAndSimilarRecommendations': '智能推荐歌单与相似歌曲', + 'routePlanningAndCongestionAvoidance': '路线规划与拥堵规避', + 'voiceBroadcastAndWaypoints': '语音播报与途径点设置', + 'realTimeWeatherForecast': '实时天气与预报', + 'clothingAdviceAndTravelReminders': '穿衣建议与出行提醒', + 'poiSearchFoodGasParking': '餐饮/加油/停车等地点检索', + 'ratingsHoursAndDirectRoutes': '评分、营业时间与路线直达', + 'hotNewsAndTopicOverview': '热点快讯与专题速览', + 'longArticleSummariesAndKeyPoints': '长文总结与要点提炼', + 'helpCallAndSMS': '帮忙拨打电话与发短信', + 'templatedMessagesAndScheduleReminders': '模板消息与日程提醒', + 'aiDisclaimer': '以上功能受设备权限、服务可用性与网络状况影响,具体以实际表现为准。', + + // 会员商品页 + 'benefitsOverview': '权益总览', + 'higherQuota': '更高使用额度', + 'lowerUnitPrice': '更低单价', + 'priorityService': '优先服务', + 'exclusiveSupportChannel': '专属客服通道', + 'fastResponse': '问题快速响应', + 'eventsAndDiscounts': '活动与折扣', + 'memberExclusiveEvents': '会员专属活动', + 'longTermDiscountPlan': '长期优惠计划', + 'generalPrivileges': '通用特权', + 'earlyAccessNewFeatures': '新功能优先体验', + 'crossDeviceSync': '跨设备同步', + 'vipDisclaimer': '具体权益以实际套餐说明为准,可能随版本迭代调整。', + + // 新手引导 / Onboarding + 'skip': '跳过', + 'next': '下一步', + 'sayLilyHint': '对着麦克风说:“Lily Lily”', + 'imHere': '我在呢~', + 'guideWelcomeWakeAI': '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— Lily', + 'aiReplyCompleted': 'Lily已经完成了回答!', + 'aiThinking': 'Lily正在思考中~', + 'guideAwakenSuccess': '太棒了!你已成功唤醒Lily,接下来试试让它帮你做件事吧', + 'confirmInput': '确定输入', + 'guideClickSendPrefix': '点击发送,', + // AI 引导步骤 + 'guideWeatherStart': '对Lily说:“今天天气怎么样”', + 'guideWeatherInput': '今天天气怎么样', + 'guideWeatherSend': 'Lily会为你实时为你播报天气', + 'guideWeatherEnd': 'Lily能实时为你播报天气,试试其他指令吧', + 'guideMusicStart': '对Lily说:“请播放一组摇滚乐”', + 'guideMusicInput': '请播放一组摇滚乐', + 'guideMusicSend': 'Lily会为你搜寻到一组音乐', + 'guideMusicEnd': 'Lily已成功为您搜寻到一组音乐,试试其他指令吧', + 'guideNavStart': '对Lily说:“导航到深圳机场”', + 'guideNavInput': '导航到深圳机场', + 'guideNavSend': 'Lily会为你规划路线', + 'guideNavEnd': 'Lily已成功为您导航到深圳机场,试试其他指令吧', + 'guideMeetingStart': '对Lily说:“明天下午3点创建会议提醒”', + 'guideMeetingInput': '明天下午3点创建会议提醒', + 'guideMeetingSend': 'Lily会为你创建会议提醒', + 'guideMeetingEnd': 'Lily已成功为您创建会议提醒,试试其他指令吧', + 'guidePhoneStart': '对Lily说:“拨打妈妈的电话”', + 'guidePhoneInput': '拨打妈妈的电话', + 'guidePhoneSend': 'Lily会为你拨打电话', + 'guidePhoneEnd': 'Lily已成功为您拨打电话,试试其他指令吧', + 'guideSmsStart': '对Lily说:“给爸爸发短信说晚上回家吃饭”', + 'guideSmsInput': '给爸爸发短信说晚上回家吃饭', + 'guideSmsSend': 'Lily会为你发送短信', + 'guideSmsEnd': '短信已发送,语音操控更便捷', // 商务会议助手页 "startMeeting": "开始会议", @@ -415,7 +592,14 @@ const Map zhCN = { // 设置页 "systemSettings": "系统设置", - "premiumMember": "高级会员", + "vipRecharge": "会员充值", + "aiRecharge": "AI充值", + "translationRecharge": "翻译充值", + "meetingAssistantRecharge": "会议充值", + "cloudSync": "云端同步", + "memberService": "会员服务", + "bindRewardTitle": "奖励", + "bindRewardSimpleDesc": "首次绑定设备奖励已下发,点击确定关闭即可", "joinDevice": "已连接设备:", "accountId": "账号ID:", "systemSetting": "系统设置", @@ -488,7 +672,7 @@ const Map zhCN = { //Agent 模块 // agent View - "XiaoYan": "小言", + "Lily": "Lily", "clearChatHistory": "清空聊天记录", "online": "在线", "inputMessage": "输入消息...", @@ -529,7 +713,7 @@ const Map zhCN = { "nearbyFood": "附近有哪些美食?", // 美食 // Agent controller - 'welcomeMessage': '你好!我是**小言**,我能帮你:\n\n' + 'welcomeMessage': '你好!我是**Lily**,我能帮你:\n\n' '• 答问题和提供信息\n\n' '• 实时语音翻译\n\n' '• 推荐音乐和播客\n\n' @@ -610,6 +794,17 @@ const Map zhCN = { "friday": "周五", "saturday": "周六", "sunday": "周日", + // 首次绑定奖励弹窗(英文文案,保持一致性) + "firstBindRewardTitle": "Welcome Reward", + "firstBindRewardDesc": "You've received binding rewards.", + "vipLevelLabel": "VIP Level:", + "expiryLabel": "到期时间:", + "notActivated": "未激活", + "aiChatCreditsLabel": "AI Chat Credits:", + "translationCreditsLabel": "Translation Credits:", + "meetingCreditsLabel": "Meeting Credits:", + "ok": "OK", + // 天气描述 "sunny": "晴", "partlyCloudy": "少云", @@ -910,22 +1105,76 @@ const Map zhCN = { "phoneUser": "手机用户", "selectSourceLanguage": "选择源语言", "selectTargetLanguage": "选择目标语言", - "me": "我", //我 - "counterPart": "对方", //对方 + "me": "我", + "counterPart": "对方", + "bluetoothCallStateError": "请确保蓝牙耳机已连接到手机并处于通话状态", // 通用对话按钮与选项(用于统一语言选择弹窗等) // 以下翻译键的中文文本内容如注释所示 "autoDetect": "自动检测", // 自动检测 + // 文本翻译与语种检测 + "languageDetectionFailed": "语言检测失败", + "translationFailed": "翻译失败", + "pleaseInputText": "请输入文本", + "textTooLong": "文本过长,最多5000字符", - // 翻译历史页 (translation_history_view.dart) + // 翻译历史页 "noTranslationHistory": "暂无历史记录", + "historyRecords": "历史记录", + "clearResults": "清空结果", "clearHistory": "清除历史记录", "clearHistoryConfirm": "确定要清除所有翻译历史记录吗?此操作无法撤销。", "unknownLanguage": "未知语言", "deleteSessionRecord": "删除会话记录", "deleteSessionConfirm": "确定要删除该会话的所有翻译记录吗?此操作无法撤销。", - // 翻译录音文件列表页 (translation_list_view.dart) + // 图片翻译模块(Picture Translation) + "selectImageSource": "选择图片来源", + "imagePreview": "图片预览", + "translationResult": "翻译结果", + "recognitionResult": "识别结果", + "translatedImagePreview": "翻译后的图片预览", + "translatedImageLoadFailed": "翻译图片加载失败", + "pleaseSelectImage": "请选择图片", + "imageLoadFailed": "图片加载失败", + "imageQuality": "图片质量", + "maxSize": "最大尺寸", + "close": "关闭", + "setImageQuality": "设置图片质量", + "currentQuality": "当前质量: @value%", + "setMaxSize": "设置最大尺寸", + "currentMaxSize": "当前: @pxpx", + "waitingForAction": "等待操作", + "pleaseSelectImageToStart": "请选择图片开始翻译", + "chooseImage": "选择图片", + "openingCamera": "正在打开相机...", + "openingPhotoAlbum": "正在打开相册...", + "cancelled": "已取消", + "userCancelledSelection": "用户取消了选择", + "translatingImage": "翻译图片", + "callingImageTranslateService": "正在调用图片翻译服务...", + "savingRecord": "保存记录", + "savingTranslationResult": "正在保存翻译结果...", + "translationCompleted": "翻译完成", + "allStepsCompleted": "所有步骤已完成", + "processingFailed": "处理失败", + "imageProcessingOrTranslationFailed": "图片处理或翻译失败,请重试。\n错误信息: {error}", + "imageTooLarge": "图片大小超过4MB限制", + "imageTooLargeHint": "图片大小超过4MB限制,请降低质量或尺寸后重试", + "emptyTranslationResult": "图片翻译接口返回空结果", + "imageTranslationFailedTryLater": "图片翻译失败,可能的原因:网络异常或接口限流,请稍后重试", + "imageTranslationApiException": "图片翻译接口调用异常,请检查网络后重试", + "permissionRequired": "权限需求", + "cameraPermissionRequired": "需要相机权限才能拍照", + "photoAlbumPermissionRequired": "需要相册权限才能选择图片", + "translationDetails": "翻译详情", + "originalImage": "原图片", + "sourceLanguage": "源语言", + "targetLanguage": "目标语言", + "translationTime": "翻译时间", + "shareInDevelopment": "分享功能开发中", + + // 翻译录音文件列表页 "translateAudioFiles": "翻译录音文件", "noAudioFiles": "暂无音频文件", "calculating": "计算中...", @@ -968,7 +1217,7 @@ const Map zhCN = { "autoInputCode": "自动输入代码", "skipBinding": "跳过绑定", "rescan": "重新扫描", - "skip": "跳过", + "verifyingDevice": "正在验证设备...", "confirmBinding": "确认绑定", "associatingDevice": "正在关联设备...", @@ -1115,4 +1364,16 @@ const Map zhCN = { "justNow": "刚刚", "minutesAgo": "@minutes分钟前", "yesterday": "昨天", + // 时间扩展(图片翻译模块使用) + "daysAgo": "@days天前", + "hoursAgo": "@hours小时前", + + //补充 + "insufficientIntegral": "积分不足", + + //余额不足弹窗 + "insufficientBalanceDesc": "余额不足,请前往对应充值界面进行充值", + "recharge": "前往充值", + // 会员到期提示 + "aiMemberExpiredDesc": "AI会员已到期,请前往会员充值续费", }; diff --git a/lib/core/translations/language/zh_hk.dart b/lib/core/translations/language/zh_hk.dart index 2a2c7fbfd..535bb5f11 100644 --- a/lib/core/translations/language/zh_hk.dart +++ b/lib/core/translations/language/zh_hk.dart @@ -131,7 +131,7 @@ const Map zhHK = { "initializing": "正在初始化...", "bluetoothInitializationFailure": "藍牙服務初始化失敗", "checkingPermissions": "正在檢查權限...", - "lackPermissions": "缺少藍牙權限", + "lackPermissions": "開啟藍牙和位置權限,才能發現並連接附近的設備", "searchingDevice": "正在搜尋設備...", "findConnectedDevice": "找到已連接的設備", "notFound": "未找到已連接的藍牙設備", @@ -161,7 +161,7 @@ const Map zhHK = { "exploreSlogan1": "智能陪伴,無限可能", "exploreSlogan2": "智能眼鏡,無限可能", "exploreAi": "探索Voitrans提供的AI功能", - "aiFunction": "AI功能", + "aiFunction": "AI助手", "allRoundAssistant": "全能助理", "allRoundAssistantInfo": "幫你解決各類問題", "simultaneousTranslation": "同聲翻譯", @@ -175,20 +175,30 @@ const Map zhHK = { "healthConsultantInfo": "提供更多AI聊天功能", "conferenceAssistant": "商務會議助手", "conferenceAssistantInfo": "實時記錄會議紀要,多語言翻譯", - "studyAssistant": "語言學習助手", - "studyAssistantInfo": "發音糾正,實時練習口語", - "travelTranslator": "旅行翻譯助手", - "travelTranslatorInfo": "實時語音翻譯,旅遊景點介紹", - "beSearching": "正在搜尋:", - "noAssistantFound": "未找到對應的AI助手", + "usageStats": "用量統計", + "usageStatsInfo": "查看您的使用統計數據", "scene": "場景", - "youHaveSelected": "您選擇了:", - "newFunction": "新功能", - "youChoseTry": "您選擇了嘗試", - "aiSinger": "AI歌手模式", - "aiSingerInfo": "用你喜歡的歌手聲音演唱任何歌曲,甚至是你自己創作的歌曲", - "popularFunction": "熱門功能場景", - "tryNow": "立即嘗試", + // 商品名稱(按ID) + 'ai_001_name': 'AI基礎 100 次', + 'ai_002_name': 'AI基礎 500 次', + 'ai_003_name': 'AI基礎 1000 次', + 'meet_001_name': '會議基礎 100 次', + 'meet_002_name': '會議基礎 500 次', + 'meet_003_name': '會議基礎 1000 次', + 'trans_001_name': '翻譯基礎 100 次', + 'trans_002_name': '翻譯基礎 500 次', + 'trans_003_name': '翻譯基礎 1000 次', + + // 商品描述(按ID) + 'ai_001_desc': '含 100 次使用,涵蓋核心 AI 功能', + 'ai_002_desc': '含 500 次使用,涵蓋核心 AI 功能', + 'ai_003_desc': '含 1000 次使用,涵蓋核心 AI 功能', + 'meet_001_desc': '含 100 次會議錄音/轉寫', + 'meet_002_desc': '含 500 次會議錄音/轉寫', + 'meet_003_desc': '含 1000 次會議錄音/轉寫', + 'trans_001_desc': '含 100 次翻譯(語音/文本)', + 'trans_002_desc': '含 500 次翻譯(語音/文本)', + 'trans_003_desc': '含 1000 次翻譯(語音/文本)', // 商務會議助手頁 "startMeeting": "開始會議", @@ -204,8 +214,12 @@ const Map zhHK = { 'meetingTranscript': '會議原文', // 設定頁 - "systemSettings": "系統設定", - "premiumMember": "高級會員", + "systemSettings": "系統設定", //系统设置 + "vipRecharge": "高級會員充值", //会员充值 + "aiRecharge": "AI充值", //AI充值 + "translationRecharge": "翻譯充值", //翻译充值 + "meetingAssistantRecharge": "會議充值", //会议充值 + "cloudSync": "雲端同步", //云端同步 "joinDevice": "已連接設備:", "accountId": "帳號ID:", "systemSetting": "系統設定", @@ -270,7 +284,7 @@ const Map zhHK = { //Agent 模塊 // agent View - "XiaoYan": "小言", + "Lily": "Lily", "clearChatHistory": "清空聊天記錄", "online": "在線", "inputMessage": "輸入訊息...", @@ -299,7 +313,7 @@ const Map zhHK = { "voice_interrupt": "语音打断", // 语音打断 // Agent controller - 'welcomeMessage': '你好!我是**小言**,我能幫你:\n\n' + 'welcomeMessage': '你好!我是**Lily**,我能幫你:\n\n' '• 回答問題和提供資訊\n\n' '• 實時語音翻譯\n\n' '• 推薦音樂和播客\n\n' @@ -787,4 +801,105 @@ const Map zhHK = { "乌尔都语": "烏爾都語", "波斯语": "波斯語", "斯瓦希里语": "斯瓦希里語", + // Guide overlay keys (placeholders in English) + "sayLilyHint": "Say \"Lily Lily\" to the mic", + "imHere": "I’m here~", + "guideWelcomeWakeAI": "Welcome to Voitrans! First, wake your AI “Lily”.", + "aiReplyCompleted": "Lily has finished the reply!", + "aiThinking": "Lily is thinking…", + "guideAwakenSuccess": "Great! Lily is awake—ask it to help.", + "confirmInput": "Confirm Input", + "guideClickSendPrefix": "Tap send, ", + "guideWeatherStart": "Tell Lily: “What’s the weather today?”", + "guideWeatherInput": "What’s the weather today?", + "guideWeatherSend": "Lily will report the weather in real time", + "guideWeatherEnd": "Lily can report weather live—try other commands", + "guideMusicStart": "Tell Lily: “Play some rock music”", + "guideMusicInput": "Play some rock music", + "guideMusicSend": "Lily will find music for you", + "guideMusicEnd": "Lily found music—try other commands", + "guideNavStart": "Tell Lily: “Navigate to Shenzhen Airport”", + "guideNavInput": "Navigate to Shenzhen Airport", + "guideNavSend": "Lily will plan the route", + "guideNavEnd": "Navigation set—try other commands", + "guideMeetingStart": + "Tell Lily: “Create a meeting reminder for 3pm tomorrow”", + "guideMeetingInput": "Create a meeting reminder for 3pm tomorrow", + "guideMeetingSend": "Lily will create a meeting reminder", + "guideMeetingEnd": "Reminder created—try other commands", + "guidePhoneStart": "Tell Lily: “Call Mom”", + "guidePhoneInput": "Call Mom", + "guidePhoneSend": "Lily will make the call", + "guidePhoneEnd": "Call placed—try other commands", + "guideSmsStart": "Tell Lily: “Send Dad a text: I’ll be home for dinner”", + "guideSmsInput": "Text Dad: I’ll be home for dinner", + "guideSmsSend": "Lily will send the message", + "guideSmsEnd": "Message sent—voice control is convenient", + + // 商品页通用页脚 + 'appNameProvider': '應用名稱:Voitrans · 服務提供:Voitrans', + 'iapCustomerServiceQQLabel': '內购商品专属客服QQ:@qq', + 'copyrightVoitrans': '© 2025 Voitrans Technologies', + + // Meeting goods page (placeholders) + 'meetingHeaderDesc1': '錄音轉寫、關鍵詞檢索與自動總結,提升會議效率。', + 'meetingHeaderDesc2': '適用於商務會議、課堂筆記、訪談紀要等場景。', + 'meetingAutoRecording': '自動錄音與分段', + 'meetingSpeakerTagging': '講話人標注', + 'meetingLongAudioTranscription': '長音頻轉寫', + 'meetingKeyPointSummary': '關鍵要點提煉', + 'meetingTopicNameSearch': '按主題/人名檢索', + 'meetingTimelineJump': '時間線跳轉', + 'meetingExportSummary': '導出紀要', + 'meetingMultiPlatformShare': '多平台分享', + 'meetingDisclaimer': '建議在安靜環境使用,效果受麥克風與網絡影響,具體以實際表現為準。', + + // Translation goods page (placeholders) + 'transHeaderDesc1': '多語種實時同傳與文本翻譯,低延時語音播報。', + 'transHeaderDesc2': '適用於出行、商務、演講與教學等場景。', + 'translateZhEn': '中英互譯', + 'lowLatencyVoiceBroadcast': '低延時語音播報', + 'multiLangTextTranslation': '多語種文本翻譯', + 'termOptimizationContextPreserve': '術語優化與語境保持', + 'lexiconTourBusinessLife': '旅遊/商務/生活詞庫', + 'personalVocabularyMemory': '專屬詞彙記憶', + 'conversationSubtitleLargeText': '對話字幕/大字模式', + 'colorHighContrast': '顏色高對比度', + 'transDisclaimer': '翻譯效果受麥克風、網絡時延與環境噪聲影響,具體以實際表現為準。', + + // AI 商品頁描述(佔位) + 'aiHeaderDesc1': '更長對話額度、更多工具調用、更強上下文長度支持。', + 'aiHeaderDesc2': '適用於智能體問答、總結分析、任務執行等場景。', + 'musicSearchPlaybackControl': '檢索曲庫、播放控制', + 'smartPlaylistAndSimilarRecommendations': '智能推薦歌單與相似歌曲', + 'routePlanningAndCongestionAvoidance': '路線規劃與擁堵規避', + 'voiceBroadcastAndWaypoints': '語音播報與途徑點設置', + 'realTimeWeatherForecast': '實時天氣與預報', + 'clothingAdviceAndTravelReminders': '穿衣建議與出行提醒', + 'poiSearchFoodGasParking': '餐飲/加油/停車等地點檢索', + 'ratingsHoursAndDirectRoutes': '評分、營業時間與路線直達', + 'hotNewsAndTopicOverview': '熱點快訊與專題速覽', + 'longArticleSummariesAndKeyPoints': '長文總結與要點提煉', + 'helpCallAndSMS': '幫忙撥打電話與發短信', + 'templatedMessagesAndScheduleReminders': '模板消息與日程提醒', + 'aiDisclaimer': '以上功能受設備權限、服務可用性與網絡狀況影響,具體以實際表現為準。', + + // VIP goods page (placeholders) + 'benefitsOverview': '權益總覽', + 'higherQuota': '更高使用額度', + 'lowerUnitPrice': '更低單價', + 'priorityService': '優先服務', + 'exclusiveSupportChannel': '專屬客服通道', + 'fastResponse': '問題快速響應', + 'eventsAndDiscounts': '活動與折扣', + 'memberExclusiveEvents': '會員專屬活動', + 'longTermDiscountPlan': '長期優惠計劃', + 'generalPrivileges': '通用特權', + 'earlyAccessNewFeatures': '新功能優先體驗', + 'crossDeviceSync': '跨設備同步', + 'vipDisclaimer': '具體權益以實際套餐說明為準,可能隨版本迭代調整。', + + //余额不足弹窗 + "insufficientBalanceDesc": "餘額不足,請前往對應充值界面進行充值", + "recharge": "去充值", }; diff --git a/lib/core/widgets/intro_overlay_builder.dart b/lib/core/widgets/intro_overlay_builder.dart index 22b52a81a..abbcb5471 100644 --- a/lib/core/widgets/intro_overlay_builder.dart +++ b/lib/core/widgets/intro_overlay_builder.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_intro/flutter_intro.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; import 'package:lottie/lottie.dart'; class IntroOverlayBuilder { @@ -19,7 +20,7 @@ class IntroOverlayBuilder { static Widget homeBuilder( StepWidgetParams params, bool isWakeup, { - String title = '对着麦克风说:“小言小言”', + String title = 'sayLilyHint', Function? onNext, }) { return Column( @@ -29,7 +30,7 @@ class IntroOverlayBuilder { margin: EdgeInsets.only(top: ScreenUtil().statusBarHeight), alignment: Alignment.centerRight, child: _button( - '跳过', + 'skip'.tr, () { params.onFinish(); }, @@ -47,7 +48,7 @@ class IntroOverlayBuilder { Padding( padding: EdgeInsets.only(bottom: 10.w), child: Text( - isWakeup ? '我在呢~' : title, + isWakeup ? 'imHere'.tr : title.tr, style: TextStyle( fontSize: 15.sp, color: Colors.white, @@ -57,7 +58,7 @@ class IntroOverlayBuilder { ), if (onNext != null) _button( - '下一步', + 'next'.tr, () { params.onNext!(); onNext(); @@ -70,7 +71,7 @@ class IntroOverlayBuilder { static Widget aiBuilder( StepWidgetParams params, String text, { - String buttonText = '下一步', + String buttonText = 'next', bool alignment = true, Function? onNext, Function? onFinish, @@ -92,7 +93,7 @@ class IntroOverlayBuilder { margin: EdgeInsets.only(top: ScreenUtil().statusBarHeight), alignment: Alignment.centerRight, child: _button( - '跳过', + 'skip'.tr, () { params.onFinish(); }, @@ -125,7 +126,7 @@ class IntroOverlayBuilder { Padding( padding: EdgeInsets.only(top: 10.w, left: 3, right: 3), child: _button( - buttonText, + buttonText.tr, () { params.onNext!(); onNext(); @@ -136,7 +137,7 @@ class IntroOverlayBuilder { Padding( padding: EdgeInsets.only(top: 10.w, left: 3, right: 3), child: _button( - buttonText, + buttonText.tr, () { params.onFinish(); onFinish(); diff --git a/lib/data/models/appconfig_model.dart b/lib/data/models/appconfig_model.dart index 65ea7838e..e656a62c9 100644 --- a/lib/data/models/appconfig_model.dart +++ b/lib/data/models/appconfig_model.dart @@ -79,6 +79,7 @@ class DBProduct { final String? productimage; final String scanuuid; final String? version; + bool? isforceupdate; final bool? iscontinuouschat; final String? updatedescription; final String? updatepackageaddress; @@ -91,6 +92,7 @@ class DBProduct { required this.productimage, required this.scanuuid, required this.version, + required this.isforceupdate, //该产品固件版本是否强制更新 required this.iscontinuouschat, required this.updatedescription, required this.updatepackageaddress, diff --git a/lib/data/models/appconfig_model.g.dart b/lib/data/models/appconfig_model.g.dart index f99ba35c6..fc97723ed 100644 --- a/lib/data/models/appconfig_model.g.dart +++ b/lib/data/models/appconfig_model.g.dart @@ -79,6 +79,7 @@ DBProduct _$DBProductFromJson(Map json) => DBProduct( productimage: json['productimage'] as String?, scanuuid: json['scanuuid'] as String, version: json['version'] as String?, + isforceupdate: json['isforceupdate'] as bool?, iscontinuouschat: json['iscontinuouschat'] as bool?, updatedescription: json['updatedescription'] as String?, updatepackageaddress: json['updatepackageaddress'] as String?, @@ -91,6 +92,7 @@ Map _$DBProductToJson(DBProduct instance) => { 'productimage': instance.productimage, 'scanuuid': instance.scanuuid, 'version': instance.version, + 'isforceupdate': instance.isforceupdate, 'iscontinuouschat': instance.iscontinuouschat, 'devicename': instance.devicename, 'updatedescription': instance.updatedescription, diff --git a/lib/data/models/channel_app_model.dart b/lib/data/models/channel_app_model.dart index dac792f28..f4a117fe3 100644 --- a/lib/data/models/channel_app_model.dart +++ b/lib/data/models/channel_app_model.dart @@ -69,12 +69,22 @@ class AppInfo { @JsonKey(name: 'tourists') final bool tourists; + /// 是否允许跳过设备绑定 + @JsonKey(name: 'allowskipdevicebinding') + final bool allowskipdevicebinding; + + /// 支付渠道列表 + @JsonKey(name: 'Paychannels') + final String? paychannels; + const AppInfo({ this.address, required this.channel, required this.description, required this.version, this.tourists = false, + this.allowskipdevicebinding = false, + required this.paychannels, }); factory AppInfo.fromJson(Map json) => diff --git a/lib/data/models/channel_app_model.g.dart b/lib/data/models/channel_app_model.g.dart index 952f3fc41..720e9cc33 100644 --- a/lib/data/models/channel_app_model.g.dart +++ b/lib/data/models/channel_app_model.g.dart @@ -38,6 +38,8 @@ AppInfo _$AppInfoFromJson(Map json) => AppInfo( description: json['description'] as String, version: json['version'] as String, tourists: json['tourists'] as bool? ?? false, + allowskipdevicebinding: json['allowskipdevicebinding'] as bool? ?? false, + paychannels: json['paychannels'] as String?, ); Map _$AppInfoToJson(AppInfo instance) => { @@ -46,4 +48,6 @@ Map _$AppInfoToJson(AppInfo instance) => { 'description': instance.description, 'version': instance.version, 'tourists': instance.tourists, + 'allowskipsevicebinding': instance.allowskipdevicebinding, + 'paychannels': instance.paychannels, }; diff --git a/lib/data/models/goods.dart b/lib/data/models/goods.dart new file mode 100644 index 000000000..0738a7a60 --- /dev/null +++ b/lib/data/models/goods.dart @@ -0,0 +1,239 @@ +/// 商品类型 +/// 0: 普通产品 normal +/// 1: 会员产品 member +import 'dart:convert'; + +// 使用量类型(与后端/Proto枚举对应) +enum UsageType { + /// 未知使用量类型 + UsageNull, // 0 + /// 翻译 + Translation, // 1 + /// 会议 + Meet, // 2 + /// 智能体 + AI, // 3 + /// 智能体 + Vip; // 4 + /// Vip + + // 解析支持 int 或 string(大小写不敏感) + static UsageType from(dynamic value) { + if (value is int) { + switch (value) { + case 0: + return UsageType.UsageNull; + case 1: + return UsageType.Translation; + case 2: + return UsageType.Meet; + case 3: + return UsageType.AI; + case 4: + return UsageType.Vip; + default: + return UsageType.UsageNull; + } + } + if (value is String) { + final v = int.tryParse(value.trim()); + if (v != null) return UsageType.from(v); + switch (value.trim().toLowerCase()) { + case 'usagenull': + case 'null': + case 'unknown': + return UsageType.UsageNull; + case 'translation': + return UsageType.Translation; + case 'meet': + case 'meeting': + return UsageType.Meet; + case 'ai': + return UsageType.AI; + case 'vip': + return UsageType.Vip; + default: + return UsageType.UsageNull; + } + } + return UsageType.UsageNull; + } + + int get toInt { + switch (this) { + case UsageType.UsageNull: + return 0; + case UsageType.Translation: + return 1; + case UsageType.Meet: + return 2; + case UsageType.AI: + return 3; + case UsageType.Vip: + return 4; + } + } + + String get name { + switch (this) { + case UsageType.UsageNull: + return 'UsageNull'; + case UsageType.Translation: + return 'Translation'; + case UsageType.Meet: + return 'Meet'; + case UsageType.AI: + return 'AI'; + case UsageType.Vip: + return 'Vip'; + } + } +} + +/// 支付商品模型,与后端 DBGoods 对应 +/// - id: 产品id +/// - enable: 是否启用 +/// - name: 产品名称 +/// - desc: 产品描述 +/// - price: 产品价格(建议单位:分) +/// - usagetype: 产品类型 0:普通产品 1:会员产品 +/// - usagenum: 使用量(时长/次数/额度等) +class DBGoods { + final String id; + final bool enable; + final String name; + final String localname; + final int price; // 建议单位:分 + final UsageType usagetype; + final int usagenum; + + const DBGoods({ + required this.id, + required this.enable, + required this.name, + required this.localname, + required this.price, + required this.usagetype, + required this.usagenum, + }); + + /// 解析后端 JSON + factory DBGoods.fromJson(Map json) { + String _asString(dynamic v) { + if (v == null) return ''; + return v is String ? v : v.toString(); + } + + bool _asBool(dynamic v) { + if (v is bool) return v; + if (v is num) return v != 0; + if (v is String) { + final s = v.trim().toLowerCase(); + return s == 'true' || s == '1'; + } + return false; + } + + int _asInt(dynamic v) { + if (v == null) return 0; + if (v is int) return v; + if (v is num) return v.toInt(); + if (v is String) return int.tryParse(v) ?? 0; + return 0; + } + + return DBGoods( + id: _asString(json['id']), + enable: _asBool(json['enable']), + name: _asString(json['name']), + localname: _asString(json['localname']), + price: _asInt(json['price']), + usagetype: UsageType.from(json['usagetype']), + usagenum: _asInt(json['usagenum']), + ); + } + + /// 编码为后端 JSON + Map toJson() { + return { + 'id': id, + 'enable': enable, + 'name': name, + 'localname': localname, + 'price': price, + 'usagetype': usagetype.toInt, + 'usagenum': usagenum, + }; + } + + /// 列表解析 + static List listFromJson(dynamic data) { + if (data == null) return const []; + if (data is List) { + return data + .map((e) => DBGoods.fromJson(e as Map)) + .toList(); + } + // 某些接口返回 { list: [...] } 或 { data: [...] } + if (data is Map) { + final list = data['list'] ?? data['data'] ?? data['items']; + if (list is List) { + return list + .map((e) => DBGoods.fromJson(e as Map)) + .toList(); + } + } + return const []; + } + + /// 价格换算:分 -> 元(仅当 price 单位为分时使用) + double get priceYuan => price / 100.0; + + /// 用于支付创建订单的金额(分) + int get payAmountCents => price; + + DBGoods copyWith({ + String? id, + bool? enable, + String? name, + String? localname, + int? price, + UsageType? usagetype, + int? usagenum, + }) { + return DBGoods( + id: id ?? this.id, + enable: enable ?? this.enable, + name: name ?? this.name, + localname: localname ?? this.localname, + price: price ?? this.price, + usagetype: usagetype ?? this.usagetype, + usagenum: usagenum ?? this.usagenum, + ); + } + + @override + String toString() { + return 'DBGoods(id: $id, enable: $enable, name: $name, price: $price, usagetype: ${usagetype.name}, usagenum: $usagenum)'; + } + + Map? get _localnameMap { + if (localname.isEmpty) return null; + try { + final v = jsonDecode(localname); + if (v is Map) return v; + } catch (_) {} + return null; + } + + Map localInfo(String tag) { + final m = _localnameMap; + if (m == null) return const {}; + final key1 = tag; + final key2 = tag.replaceAll('_', '-'); + final key3 = tag.replaceAll('-', '_'); + dynamic e = m[key1] ?? m[key2] ?? m[key3]; + if (e == null && m.isNotEmpty) e = m.values.first; + return e is Map ? e as Map : const {}; + } +} diff --git a/lib/data/models/meeting_usage_stats.dart b/lib/data/models/meeting_usage_stats.dart new file mode 100644 index 000000000..4568b488a --- /dev/null +++ b/lib/data/models/meeting_usage_stats.dart @@ -0,0 +1,292 @@ +///会议模块用量统计的模型类 + +// ============== ASR 状态常量 ============== +const int asrStatusProcessing = 0; // 处理中 +const int asrStatusFailed = 1; // 处理失败 +const int asrStatusCompleted = 2; // 已完成 + +// ============== ASR 单次记录 ============== +// 会议音频文件转文字单次数据 +class MeetingAsrRecord { + String id; // 本次ASR唯一标识 + int meetingId; // 会议记录ID + String taskId; // 云端任务ID + int audioSeconds; // 音频总时长(秒) + int fileSizeBytes; // 音频文件大小(字节) + String language; // ASR语言 + int submitTimestamp; // 提交时间戳(毫秒) + + int status; // 状态: 0-处理中, 1-处理失败, 2-已完成 + int transcriptBytes; // 转写后的字节数(utf8) + int completeTimestamp; // 完成时间戳(毫秒) + + MeetingAsrRecord({ + required this.id, + required this.meetingId, + required this.taskId, + required this.audioSeconds, + required this.fileSizeBytes, + required this.language, + required this.submitTimestamp, + required this.status, + required this.transcriptBytes, + required this.completeTimestamp, + }); + + Map toJson() => { + 'id': id, + 'meetingId': meetingId, + 'taskId': taskId, + 'audioSeconds': audioSeconds, + 'fileSizeBytes': fileSizeBytes, + 'language': language, + 'submitTimestamp': submitTimestamp, + 'status': status, + 'transcriptBytes': transcriptBytes, + 'completeTimestamp': completeTimestamp, + }; + + factory MeetingAsrRecord.fromJson(Map json) { + return MeetingAsrRecord( + id: json['id'] ?? '', + meetingId: json['meetingId'] ?? 0, + taskId: json['taskId'] ?? '', + audioSeconds: json['audioSeconds'] ?? 0, + fileSizeBytes: json['fileSizeBytes'] ?? 0, + language: json['language'] ?? '', + submitTimestamp: json['submitTimestamp'] ?? 0, + status: json['status'] ?? 0, + transcriptBytes: json['transcriptBytes'] ?? 0, + completeTimestamp: json['completeTimestamp'] ?? 0, + ); + } +} + +// ============== 对象存储单次记录 ============== +// 对象存储单次上传记录 +class MeetingObjectStorageRecord { + int meetingId; // 会议记录ID + String storageUrl; // 云端存储URL + int storageBytes; // 文件大小(字节) + int transferBytes; // 实际传输字节数 + int uploadTimestamp; // 上传完成时间戳(毫秒) + + MeetingObjectStorageRecord({ + required this.meetingId, + required this.storageUrl, + required this.storageBytes, + required this.transferBytes, + required this.uploadTimestamp, + }); + + Map toJson() => { + 'meetingId': meetingId, + 'storageUrl': storageUrl, + 'storageBytes': storageBytes, + 'transferBytes': transferBytes, + 'uploadTimestamp': uploadTimestamp, + }; + + factory MeetingObjectStorageRecord.fromJson(Map json) { + int _asInt(dynamic v) { + if (v == null) return 0; + if (v is int) return v; + if (v is num) return v.toInt(); + if (v is String) return int.tryParse(v) ?? 0; + return 0; + } + + return MeetingObjectStorageRecord( + meetingId: _asInt(json['meetingId']), + storageUrl: (json['storageUrl'] ?? '').toString(), + storageBytes: _asInt(json['storageBytes']), + transferBytes: _asInt(json['transferBytes']), + uploadTimestamp: _asInt(json['uploadTimestamp']), + ); + } +} + +// 会议音频文件转文字单日统计 +class MeetingAsrDailyStats { + String date; // 日期字符串: YYYY-MM-DD + String userId; // 用户ID + List items; // 当天所有单次ASR + + MeetingAsrDailyStats({ + required this.date, + required this.userId, + required this.items, + }); + + // 计算型聚合,不冗余存储 + int get asrCalls => items.length; + int get audioSecondsTotal => items.fold(0, (s, r) => s + r.audioSeconds); + int get audioBytesTotal => items.fold(0, (s, r) => s + r.fileSizeBytes); + int get transcriptBytesTotal => + items.fold(0, (s, r) => s + r.transcriptBytes); + + Map toJson() => { + 'date': date, + 'userId': userId, + 'items': items.map((e) => e.toJson()).toList(), + }; + + factory MeetingAsrDailyStats.fromJson(Map json) { + final list = (json['items'] as List? ?? []) + .map((e) => MeetingAsrRecord.fromJson(Map.from(e))) + .toList(); + return MeetingAsrDailyStats( + date: json['date'] ?? '', + userId: json['userId'] ?? 'anonymous', + items: list, + ); + } +} + +// ============== 对象存储单日统计 ============== +// 👇 职责单一:仅统计对象存储相关数据 +class MeetingStorageDailyStats { + String date; // 日期字符串: YYYY-MM-DD + String userId; // 用户ID + List items; // 当天所有单次对象存储记录 + + MeetingStorageDailyStats({ + required this.date, + required this.userId, + required this.items, + }); + + // 对象存储 聚合计算 + int get storageUploads => items.length; + int get storageBytesTotal => items.fold(0, (s, r) => s + r.storageBytes); + int get transferBytesTotal => items.fold(0, (s, r) => s + r.transferBytes); + + Map toJson() => { + 'date': date, + 'userId': userId, + 'items': items.map((e) => e.toJson()).toList(), + }; + + factory MeetingStorageDailyStats.fromJson(Map json) { + final storageList = (json['items'] as List? ?? []) + .map((e) => + MeetingObjectStorageRecord.fromJson(Map.from(e))) + .toList(); + + return MeetingStorageDailyStats( + date: json['date'] ?? '', + userId: json['userId'] ?? 'anonymous', + items: storageList, + ); + } +} + +// 会议音频文件转文字历史统计(所有的用量) +class MeetingAsrHistoryStats { + String userId; // 用户ID + int allAsrCalls; // 所有ASR调用次数 + int allAudioSeconds; // 所有音频总时长(秒) + int allAudioFileBytes; // 所有音频文件大小(字节) + int allTranscriptBytes; // 所有转写后的字节数(utf8) + + MeetingAsrHistoryStats({ + required this.userId, + required this.allAsrCalls, + required this.allAudioSeconds, + required this.allAudioFileBytes, + required this.allTranscriptBytes, + }); + + Map toJson() => { + 'userId': userId, + 'allAsrCalls': allAsrCalls, + 'allAudioSeconds': allAudioSeconds, + 'allAudioFileBytes': allAudioFileBytes, + 'allTranscriptBytes': allTranscriptBytes, + }; + + factory MeetingAsrHistoryStats.fromJson(Map json) { + int _asInt(dynamic v) { + if (v == null) return 0; + if (v is int) return v; + if (v is num) return v.toInt(); + if (v is String) return int.tryParse(v) ?? 0; + return 0; + } + + return MeetingAsrHistoryStats( + userId: json['userId'] ?? 'anonymous', + allAsrCalls: _asInt(json['allAsrCalls']), + allAudioSeconds: _asInt(json['allAudioSeconds']), + allAudioFileBytes: _asInt(json['allAudioFileBytes']), + allTranscriptBytes: _asInt(json['allTranscriptBytes']), + ); + } +} + +// ============== 对象存储历史统计 ============== +// 👇 职责单一:仅统计对象存储累计数据 +class MeetingStorageHistoryStats { + String userId; // 用户ID + int allStorageUploads; // 所有对象存储上传次数 + int allStorageBytes; // 所有存储文件总大小(字节) + int allTransferBytes; // 所有传输总字节数 + + MeetingStorageHistoryStats({ + required this.userId, + required this.allStorageUploads, + required this.allStorageBytes, + required this.allTransferBytes, + }); + + Map toJson() => { + 'userId': userId, + 'allStorageUploads': allStorageUploads, + 'allStorageBytes': allStorageBytes, + 'allTransferBytes': allTransferBytes, + }; + + factory MeetingStorageHistoryStats.fromJson(Map json) { + int _asInt(dynamic v) { + if (v == null) return 0; + if (v is int) return v; + if (v is num) return v.toInt(); + if (v is String) return int.tryParse(v) ?? 0; + return 0; + } + + return MeetingStorageHistoryStats( + userId: json['userId'] ?? 'anonymous', + allStorageUploads: _asInt(json['allStorageUploads']), + allStorageBytes: _asInt(json['allStorageBytes']), + allTransferBytes: _asInt(json['allTransferBytes']), + ); + } +} + +// 👇 当需要同时展示两种统计时使用 +class MeetingUsageDailySnapshot { + String date; + String userId; + MeetingAsrDailyStats? asrStats; + MeetingStorageDailyStats? storageStats; + + MeetingUsageDailySnapshot({ + required this.date, + required this.userId, + this.asrStats, + this.storageStats, + }); +} + +class MeetingUsageHistorySnapshot { + String userId; + MeetingAsrHistoryStats? asrStats; + MeetingStorageHistoryStats? storageStats; + + MeetingUsageHistorySnapshot({ + required this.userId, + this.asrStats, + this.storageStats, + }); +} diff --git a/lib/data/models/translation_usage_stats.dart b/lib/data/models/translation_usage_stats.dart index 6d73b1958..db420c771 100644 --- a/lib/data/models/translation_usage_stats.dart +++ b/lib/data/models/translation_usage_stats.dart @@ -1,5 +1,3 @@ - - /// 翻译模式统计数据 class TranslationModeStats { final int apiCalls; // API调用次数 @@ -46,7 +44,8 @@ class TranslationModeStats { return TranslationModeStats( apiCalls: apiCalls ?? this.apiCalls, sourceCharacters: sourceCharacters ?? this.sourceCharacters, - effectiveAudioDuration: effectiveAudioDuration ?? this.effectiveAudioDuration, + effectiveAudioDuration: + effectiveAudioDuration ?? this.effectiveAudioDuration, ttsCharacters: ttsCharacters ?? this.ttsCharacters, cost: cost ?? this.cost, ); @@ -158,7 +157,7 @@ class TranslationUsageStats { final double estimatedCost; // 预估成本 final String currency; // 货币单位 final Map costBreakdown; // 成本分解(ASR/TTS/Translation) - + TranslationUsageStats({ required this.id, required this.date, @@ -196,7 +195,8 @@ class TranslationUsageStats { final languagePairStatsMap = {}; if (json['languagePairStats'] != null) { - final languagePairStatsJson = json['languagePairStats'] as Map; + final languagePairStatsJson = + json['languagePairStats'] as Map; languagePairStatsJson.forEach((key, value) { languagePairStatsMap[key] = LanguagePairStats.fromJson(value); }); @@ -219,9 +219,15 @@ class TranslationUsageStats { asrApiCalls: json['asrApiCalls'] ?? 0, ttsApiCalls: json['ttsApiCalls'] ?? 0, aiSummaryApiCalls: json['aiSummaryApiCalls'] ?? 0, - sourceTextCharacters: json['sourceTextCharacters'] ?? json['sourceCharacterCount'] ?? 0, // 兼容旧字段 - effectiveAudioDuration: json['effectiveAudioDuration'] ?? json['audioRecordingDuration'] ?? 0, // 兼容旧字段 - ttsTextCharacters: json['ttsTextCharacters'] ?? json['targetCharacterCount'] ?? 0, // 兼容旧字段 + sourceTextCharacters: json['sourceTextCharacters'] ?? + json['sourceCharacterCount'] ?? + 0, // 兼容旧字段 + effectiveAudioDuration: json['effectiveAudioDuration'] ?? + json['audioRecordingDuration'] ?? + 0, // 兼容旧字段 + ttsTextCharacters: json['ttsTextCharacters'] ?? + json['targetCharacterCount'] ?? + 0, // 兼容旧字段 totalSessionDuration: json['totalSessionDuration'] ?? 0, asrNetworkBytes: json['asrNetworkBytes'] ?? 0, ttsNetworkBytes: json['ttsNetworkBytes'] ?? 0, @@ -328,12 +334,14 @@ class TranslationUsageStats { ttsApiCalls: ttsApiCalls ?? this.ttsApiCalls, aiSummaryApiCalls: aiSummaryApiCalls ?? this.aiSummaryApiCalls, sourceTextCharacters: sourceTextCharacters ?? this.sourceTextCharacters, - effectiveAudioDuration: effectiveAudioDuration ?? this.effectiveAudioDuration, + effectiveAudioDuration: + effectiveAudioDuration ?? this.effectiveAudioDuration, ttsTextCharacters: ttsTextCharacters ?? this.ttsTextCharacters, totalSessionDuration: totalSessionDuration ?? this.totalSessionDuration, asrNetworkBytes: asrNetworkBytes ?? this.asrNetworkBytes, ttsNetworkBytes: ttsNetworkBytes ?? this.ttsNetworkBytes, - translationNetworkBytes: translationNetworkBytes ?? this.translationNetworkBytes, + translationNetworkBytes: + translationNetworkBytes ?? this.translationNetworkBytes, sessionCount: sessionCount ?? this.sessionCount, modeStats: modeStats ?? this.modeStats, languagePairStats: languagePairStats ?? this.languagePairStats, @@ -345,7 +353,8 @@ class TranslationUsageStats { } /// 获取总API调用次数 - int get totalApiCalls => translationApiCalls + asrApiCalls + ttsApiCalls + aiSummaryApiCalls; + int get totalApiCalls => + translationApiCalls + asrApiCalls + ttsApiCalls + aiSummaryApiCalls; /// 获取总字符数(精确计费基础) int get totalCharacterCount => sourceTextCharacters + ttsTextCharacters; @@ -354,19 +363,21 @@ class TranslationUsageStats { int get totalAudioDuration => effectiveAudioDuration + totalSessionDuration; /// 获取总网络流量 - int get totalNetworkBytes => asrNetworkBytes + ttsNetworkBytes + translationNetworkBytes; + int get totalNetworkBytes => + asrNetworkBytes + ttsNetworkBytes + translationNetworkBytes; /// 获取最常用的语言对 String get mostUsedLanguagePair { - if (languagePairStats.isEmpty) return '无数据'; + if (languagePairStats.isEmpty) return 'null'; return languagePairStats.entries - .reduce((a, b) => a.value.translationCount > b.value.translationCount ? a : b) + .reduce((a, b) => + a.value.translationCount > b.value.translationCount ? a : b) .key; } /// 获取最常用的翻译模式 String get mostUsedMode { - if (modeStats.isEmpty) return '无数据'; + if (modeStats.isEmpty) return 'null'; return modeStats.entries .reduce((a, b) => a.value.apiCalls > b.value.apiCalls ? a : b) .key; diff --git a/lib/data/models/user_Info.dart b/lib/data/models/user_Info.dart index d5d369ba6..87e2cfbce 100644 --- a/lib/data/models/user_Info.dart +++ b/lib/data/models/user_Info.dart @@ -1,10 +1,14 @@ import 'package:get_storage/get_storage.dart'; +import 'package:get/get.dart'; import '../../../core/utils/logger.dart'; class User { // 单例实例 static User? _instance; + /// 全局刷新信号(当用户信息更新时 +1,用于驱动UI重建) + static final RxInt refreshTick = 0.obs; + /// 安全地检查用户是否已经登录(即单例是否已初始化) static bool isLoggedIn() { return _instance != null; @@ -41,6 +45,13 @@ class User { String phonemac; String password; int viplv = 0; //添加会员等级字段,0表示普通用户 + int vipexptime = 0; // 会员到期/剩余时间(秒/毫秒,按后端定义) + int aichatintegral = 0; // ai聊天积分 + int aichattotalintegral = 0; // ai聊天总积分 + int tradeintegral = 0; // 翻译积分 + int tradetotalintegral = 0; // 翻译总积分 + int meetintegral = 0; // 会议积分 + int meettotalintegral = 0; // 会议总积分 List devices; @@ -56,6 +67,13 @@ class User { required this.phonemac, required this.password, required this.viplv, + required this.vipexptime, + required this.aichatintegral, + required this.aichattotalintegral, + required this.tradeintegral, + required this.tradetotalintegral, + required this.meetintegral, + required this.meettotalintegral, required this.devices, }); @@ -65,51 +83,95 @@ class User { final devicesData = json['devices'] as List?; // 允许空值 //final token = json['data']['token'] as String? ?? ''; //final token = ''; - // 更新单例实例 - return _instance = User._( + final u = User._( token: '', - //token: token as String? ?? '', - name: (userData?['name'] as String?) ?? '默认名称', // 提供默认值 + name: (userData?['name'] as String?) ?? '默认名称', uid: (userData?['uid'] as String?) ?? '', avatar: (userData?['avatar'] as String?) ?? '', - language: (userData?['language'] as String?) ?? 'en', - mail: (userData?['mail'] as String?) ?? '', phone: (userData?['phone'] as String?) ?? '', phonemac: (userData?['phonemac'] as String?) ?? '', password: (userData?['password'] as String?) ?? '', viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值 + vipexptime: (userData?['vipexptime'] as int?) ?? 0, // 提供默认值 + + aichatintegral: (userData?['aichatintegral'] as int?) ?? 0, + aichattotalintegral: (userData?['aichattotalintegral'] as int?) ?? 0, + tradeintegral: (userData?['tradeintegral'] as int?) ?? 0, + tradetotalintegral: (userData?['tradetotalintegral'] as int?) ?? 0, + meetintegral: (userData?['meetintegral'] as int?) ?? 0, + meettotalintegral: (userData?['meettotalintegral'] as int?) ?? 0, devices: devicesData ?.map((e) => UserDevice.fromJson(e as Map)) .toList() ?? - [], // 处理设备列表 + [], ); + _instance = u; + Logger.i("User", + "初始化用户: uid=${u.uid}, name=${u.name}, viplv=${u.viplv}, vipexptime=${u.vipexptime}, ai聊天=${u.aichatintegral}/${u.aichattotalintegral}, 翻译=${u.tradeintegral}/${u.tradetotalintegral}, 会议=${u.meetintegral}/${u.meettotalintegral}"); + return u; } + factory User.fronTokenJson(Map json) { // 修复1:安全获取嵌套的 user 字段 final userData = json['user'] as Map?; // 允许空值 final devicesData = json['devices'] as List?; // 允许空值 - print("数据获取fronTokenJson$userData $devicesData"); - // 更新单例实例 - return _instance = User._( + Logger.d("User", + "fronTokenJson: user=$userData, devices=${devicesData?.length ?? 0}"); + final u = User._( token: '', - name: (userData?['name'] as String?) ?? '默认名称', // 提供默认值 + name: (userData?['name'] as String?) ?? '默认名称', uid: (userData?['uid'] as String?) ?? '', avatar: (userData?['avatar'] as String?) ?? '', language: (userData?['language'] as String?) ?? 'en', - mail: (userData?['mail'] as String?) ?? '', phone: (userData?['phone'] as String?) ?? '', phonemac: (userData?['phonemac'] as String?) ?? '', password: (userData?['password'] as String?) ?? '', viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值 + vipexptime: (userData?['vipexptime'] as int?) ?? 0, // 提供默认值 + aichatintegral: (userData?['aichatintegral'] as int?) ?? 0, + aichattotalintegral: (userData?['aichattotalintegral'] as int?) ?? 0, + tradeintegral: (userData?['tradeintegral'] as int?) ?? 0, + tradetotalintegral: (userData?['tradetotalintegral'] as int?) ?? 0, + meetintegral: (userData?['meetintegral'] as int?) ?? 0, + meettotalintegral: (userData?['meettotalintegral'] as int?) ?? 0, devices: devicesData != null ? (devicesData) .map((e) => UserDevice.fromJson(e as Map)) .toList() - : [], // 处理设备列表 + : [], ); + _instance = u; + Logger.i("User", + "初始化用户(token): uid=${u.uid}, name=${u.name}, viplv=${u.viplv}, vipexptime=${u.vipexptime}, ai聊天=${u.aichatintegral}/${u.aichattotalintegral}, 翻译=${u.tradeintegral}/${u.tradetotalintegral}, 会议=${u.meetintegral}/${u.meettotalintegral}"); + return u; + } + + updateUserInfo(Map user) { + name = user['name'] as String? ?? name; + uid = user['uid'] as String? ?? uid; + avatar = user['avatar'] as String? ?? avatar; + language = user['language'] as String? ?? language; + token = user['token'] as String? ?? token; + mail = user['mail'] as String? ?? mail; + phone = user['phone'] as String? ?? phone; + phonemac = user['phonemac'] as String? ?? phonemac; + password = user['password'] as String? ?? password; + viplv = user['viplv'] as int? ?? viplv; + vipexptime = user['vipexptime'] as int? ?? vipexptime; + aichatintegral = user['aichatintegral'] as int? ?? aichatintegral; + aichattotalintegral = + user['aichattotalintegral'] as int? ?? aichattotalintegral; + tradeintegral = user['tradeintegral'] as int? ?? tradeintegral; + tradetotalintegral = + user['tradetotalintegral'] as int? ?? tradetotalintegral; + meetintegral = user['meetintegral'] as int? ?? meetintegral; + meettotalintegral = user['meettotalintegral'] as int? ?? meettotalintegral; + + // 触发全局刷新信号,驱动依赖UI重建 + refreshTick.value++; } // 完整的JSON序列化 @@ -125,6 +187,13 @@ class User { 'phonemac': phonemac, 'password': password, 'viplv': viplv, + 'vipexptime': vipexptime, + 'aichatintegral': aichatintegral, + 'aichattotalintegral': aichattotalintegral, + 'tradeintegral': tradeintegral, + 'tradetotalintegral': tradetotalintegral, + 'meetintegral': meetintegral, + 'meettotalintegral': meettotalintegral, 'devices': devices.map((device) => device.toJson()).toList(), }, }; diff --git a/lib/data/services/account_service.dart b/lib/data/services/account_service.dart index 419749ee4..1293ecec5 100644 --- a/lib/data/services/account_service.dart +++ b/lib/data/services/account_service.dart @@ -6,6 +6,8 @@ import '../../modules/login/controllers/googleLogin.dart'; import '../../modules/login/controllers/facebookLogin.dart'; import '../services/network/api.dart'; import 'db/database_helper.dart'; +import 'meeting/meeting_task_service.dart'; +import '../../data/models/user_Info.dart'; /// 账号管理服务 /// @@ -32,6 +34,7 @@ class AccountService extends GetxService { await _storage.write("logintoken", null); await _storage.write("user_info", null); await _storage.write("local_avatar", null); + User.clearUserSession(); logger.d('用户数据清理成功'); return true; } catch (e) { @@ -44,6 +47,7 @@ class AccountService extends GetxService { // 清理所有用户数据 try { await _storage.erase(); + User.clearUserSession(); logger.d('所有用户数据清理成功'); return true; } catch (e) { @@ -79,6 +83,10 @@ class AccountService extends GetxService { /// 返回操作结果,包含成功状态和错误信息 Future signOut() async { try { + // 清除会议记录任务列表 + final taskService = Get.find(); + taskService.cancelTask(); + // 上传会议记录操作记录 await Synchrodata.uploadOperationRecords(); diff --git a/lib/data/services/asr_service.dart b/lib/data/services/asr_service.dart index 1a74af25c..35c263e34 100644 --- a/lib/data/services/asr_service.dart +++ b/lib/data/services/asr_service.dart @@ -20,8 +20,9 @@ abstract class AsrService { /// /// 返回一个包含识别事件的流 Future startContinuousRecognition( - bool audioSourceType, - ); + bool audioSourceType, { + bool isRemoveFirstPunctuation = true, + }); /// 停止连续语音识别 Future stopContinuousRecognition(); diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 9bcd63069..7807fb894 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -2,17 +2,20 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:get_storage/get_storage.dart'; import 'package:get/get.dart'; import 'package:ble_service/ble_service.dart'; import 'package:agent_service/agent_service.dart'; +import '../models/user_Info.dart' as userInfo; import '/data/models/appconfig_model.dart'; import '../../data/models/appconfig.dart'; import '../../core/utils/logger.dart'; import 'bluetooth_manager.dart'; import 'language_manager.dart'; +import 'network/dio_manager.dart'; // 重新导出BLE连接状态常量,让外部可以通过BleManager访问 export 'package:ble_service/ble_service.dart' show BleConnectionState; @@ -27,6 +30,67 @@ class BleCommands { static const int GET_WAKEUP_VERSION = 0x08; } +/// 判断编解码控制指令是否成功 +/// +/// 根据 rawData 第二字节是否为非 0 来判断: +/// - 成功:第二字节不为 0 +/// - 失败或无法解析:返回 false +bool _isCodecControlSuccess(Map data) { + final dynamic raw = data['rawData']; + if (raw == null) return false; + + int? secondByte; + + if (raw is List) { + if (raw.length >= 2) { + final s = raw[1]; + if (s is int) { + secondByte = s; + } else if (s is num) { + secondByte = s.toInt(); + } else if (s is String) { + final t = s.trim(); + if (t.startsWith('0x') || t.startsWith('0X')) { + secondByte = int.tryParse(t.substring(2), radix: 16); + } else { + secondByte = int.tryParse(t); + } + } + } + } else if (raw is String) { + final normalized = raw.trim().replaceAll('[', '').replaceAll(']', ''); + final parts = + normalized.split(RegExp(r'[,\s]+')).where((e) => e.isNotEmpty).toList(); + if (parts.length >= 2) { + final t = parts[1].trim(); + if (t.startsWith('0x') || t.startsWith('0X')) { + secondByte = int.tryParse(t.substring(2), radix: 16); + } else { + secondByte = int.tryParse(t); + } + } + } else if (raw is Map) { + final dynamic s = raw['second'] ?? raw['status2'] ?? raw['result']; + if (s is int) { + secondByte = s; + } else if (s is num) { + secondByte = s.toInt(); + } else if (s is String) { + final t = s.trim(); + if (t.startsWith('0x') || t.startsWith('0X')) { + secondByte = int.tryParse(t.substring(2), radix: 16); + } else { + secondByte = int.tryParse(t); + } + } + } else { + // 无法解析的类型 + return false; + } + + return secondByte != null && secondByte != 0; +} + /// 蓝牙设备信息缓存模型 class DeviceInfo { final String address; @@ -147,12 +211,12 @@ class BleManager extends GetxService { // 设备信息缓存 final deviceInfo = Rx(null); // 使用 Rx 包装 - final _wakeupVoiceUpgradeProgress = 0.obs; // 唤醒语音升级进度 - RxInt get wakeupVoiceUpgradeProgress => _wakeupVoiceUpgradeProgress; - - final _wakeupVoiceUpgradeStatus = - 0.obs; // 唤醒语音升级状态 0: 未升级 1: 升级中 2: 升级成功 3: 升级失败 - RxInt get wakeupVoiceUpgradeStatus => _wakeupVoiceUpgradeStatus; + //添加编码器状态跟踪 + final _isCodecActive = false.obs; + bool get isCodecActive => _isCodecActive.value; + RxBool get isCodecActiveRx => _isCodecActive; + bool _awaitingCodecOpen = false; //是否正在等待编码器打开的成功应答 + bool _awaitingCodecClose = false; //是否正在等待编码器关闭的成功应答 // 订阅 StreamSubscription? _statusSubscription; @@ -218,7 +282,9 @@ class BleManager extends GetxService { state == BleConnectionState.error) { Logger.info('lxm--BLE设备已断开或发生错误'); _isConnected.value = false; - // 更新设备信息Add commentMore actions + _awaitingCodecOpen = false; + _awaitingCodecClose = false; + _isCodecActive.value = false; Logger.info('lxm--断开连接时重置设备信息为0'); _updateDeviceInfoIfNeeded( batteryLevel: 0, @@ -350,7 +416,27 @@ class BleManager extends GetxService { Logger.info('lxm--处理编解码控制命令'); // 处理通话状态 print(" CONTROL_CODEC: $commandId,$data"); - //_isCall.value = data['callStatus'] as bool? ?? false; + + final bool codecSuccess = _isCodecControlSuccess(data); + if (_awaitingCodecOpen) { + //如果正在等待打开编解码器 + _isCodecActive.value = codecSuccess; // 只在等待打开时更新状态 + _awaitingCodecOpen = false; + } else if (_awaitingCodecClose) { + if (codecSuccess) { + _isCodecActive.value = false; + } + _awaitingCodecClose = false; + } + + if (codecSuccess) { + print("lxm--编解码控制成功 (rawData 第二字节非 0)"); + Logger.info('lxm--编解码控制成功 (rawData 第二字节非 0)'); + } else { + print("lxm--编解码控制失败 (rawData 为 [162, 0] 或无法解析)"); + Logger.warning('lxm--编解码控制失败 (rawData 为 [162, 0] 或无法解析)'); + } + break; case BleCommands.GET_WAKEUP_VERSION: Logger.info('lxm--处理唤醒语音版本命令'); @@ -760,10 +846,7 @@ class BleManager extends GetxService { Future startAgentService() async { try { // 从.env文件直接读取环境变量 - var serverurl = dotenv.env['SERVER_URL'] ?? ''; - if (Get.locale?.countryCode != 'CN') { - serverurl = dotenv.env['HW_SERVER_URL'] ?? ''; - } + var serverurl = DioManager().baseUrl; var jsonString = _storage.read("logintoken"); @@ -791,6 +874,9 @@ class BleManager extends GetxService { Logger.info('启动BLE服务,musicServiceType: ${musicServiceType}'); final isTtsInterruptEnabled = await _loadTtsInterrupe(); + final vipExpiryTime = userInfo.User.instance.vipexptime; + Logger.info('liwei------ vipexptime 1: $vipExpiryTime'); + // 根据APP语言动态配置语音识别语言 List supportedLanguages = []; try { @@ -861,7 +947,9 @@ class BleManager extends GetxService { 'musicServiceType': musicServiceType, "navigationMode": navigationMode, "isttsinterrupt": isTtsInterruptEnabled ? "true" : "false", - "callingTool": "callingTool".tr + "vipexptime": vipExpiryTime, //会员过期时间 + "callingToolText": "callingTool".tr, + "insufficientIntegralText": "insufficientIntegral".tr, }; // 记录日志(不记录敏感信息) @@ -995,19 +1083,27 @@ class BleManager extends GetxService { /// 打开解码器 Future openDecoder() async { try { + _awaitingCodecOpen = true; + _awaitingCodecClose = false; + _isCodecActive.value = false; return await _bleService.openDecoder(); } catch (e) { Logger.error('打开解码器失败: ${e.toString()}'); + _awaitingCodecOpen = false; return false; } } - /// 打开解码器 + /// 打开A2DP解码器 Future openA2DPDecoder() async { try { + _awaitingCodecOpen = true; + _awaitingCodecClose = false; + _isCodecActive.value = false; return await _bleService.openA2DPDecoder(); } catch (e) { Logger.error('打开解码器失败: ${e.toString()}'); + _awaitingCodecOpen = false; return false; } } @@ -1025,9 +1121,13 @@ class BleManager extends GetxService { /// 关闭编解码器 Future closeCodec() async { try { + _awaitingCodecOpen = false; + _awaitingCodecClose = true; + _isCodecActive.value = false; return await _bleService.closeCodec(); } catch (e) { Logger.error('关闭编解码器失败: ${e.toString()}'); + _awaitingCodecClose = false; return false; } } @@ -1171,6 +1271,11 @@ class BleManager extends GetxService { List? uuids, }) async { try { + // 在注册新设备前先清除旧设备信息 + if (macAddress != null && macAddress != currentDeviceAddress) { + clearDeviceInfo(); // 清除上一个设备的信息 + } + // 保存设备地址信息 if (macAddress != null) { currentDeviceAddress = macAddress; diff --git a/lib/data/services/bluetooth_manager.dart b/lib/data/services/bluetooth_manager.dart index ee28c94fa..7075fa3d4 100644 --- a/lib/data/services/bluetooth_manager.dart +++ b/lib/data/services/bluetooth_manager.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:get/get.dart'; import 'package:classic_bluetooth/classic_bluetooth.dart'; +import 'package:get_storage/get_storage.dart'; import '../../core/utils/logger.dart'; import '../../routes/app_routes.dart'; import 'ble_manager.dart'; @@ -21,6 +22,7 @@ class BluetoothManager extends GetxService { final Rx currentDeviceRx = Rx(null); BluetoothDevice? get currentDevice => currentDeviceRx.value; final BleManager _bleManager = Get.find(); + final GetStorage _storage = GetStorage(); // 订阅 StreamSubscription? _bluetoothSubscription; @@ -76,7 +78,8 @@ class BluetoothManager extends GetxService { currentDeviceRx.value = newDevice; // 检查当前是否在OTA升级界面,如果是则不跳转 - if (!_isInOtaUpgradePage() && + if (_isLoggedIn() && + !_isInOtaUpgradePage() && !_bleManager.isConnected && Get.currentRoute != Routes.pairing) { Get.toNamed(Routes.pairing, arguments: newDevice); @@ -161,4 +164,14 @@ class BluetoothManager extends GetxService { return false; } } + + /// 是否已登录 + bool _isLoggedIn() { + try { + final token = _storage.read('logintoken'); + return token != null && token is String && token.isNotEmpty; + } catch (e) { + return false; + } + } } diff --git a/lib/data/services/deapsound_ai_service.dart b/lib/data/services/deapsound_ai_service.dart index da7181f4f..311af1184 100644 --- a/lib/data/services/deapsound_ai_service.dart +++ b/lib/data/services/deapsound_ai_service.dart @@ -78,6 +78,10 @@ class DeapsoundAIService implements AiService { ..body = jsonEncode(requestBody); final response = await client.send(request); + if (response.statusCode != 200) { + throw Exception('Unknown error'); + } + await for (final chunk in response.stream .transform(utf8.decoder) .transform(const LineSplitter())) { diff --git a/lib/data/services/meeting/meeting_task_aggregator.dart b/lib/data/services/meeting/meeting_task_aggregator.dart new file mode 100644 index 000000000..5de375cda --- /dev/null +++ b/lib/data/services/meeting/meeting_task_aggregator.dart @@ -0,0 +1,105 @@ +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +import 'meeting_task_service.dart'; +import 'meeting_task_service2.dart'; + +/// 会议助手任务服务聚合器(统一列表与状态) +class MeetingTaskAggregator extends GetxService { + final GetStorage _storage = GetStorage(); + + final MeetingTaskService s1 = Get.find(); + final MeetingTaskService2 s2 = Get.find(); + + RxList taskFinishList = [].obs; + RxInt tasktype = 1.obs; + int meetingId = 0; + List textList = []; + + Worker? _w1; + Worker? _w2; + Worker? _wTasktype; + + @override + void onInit() { + super.onInit(); + _w1 = ever(s1.taskFinishList, (_) => _rebuild()); + _w2 = ever(s2.taskFinishList, (_) => _rebuild()); + _rebuild(); + } + + void setMeeting(int id) { + meetingId = id; + final String svc = + (_storage.read("meeting_service_type_$id") ?? '').toString(); + final bool isFast = svc == 'fast'; + _wTasktype?.dispose(); + if (isFast) { + _wTasktype = ever(s2.tasktype, (value) { + tasktype.value = value; + if (value == 2) { + textList = s2.textList; + } + }); + } else { + _wTasktype = ever(s1.tasktype, (value) { + tasktype.value = value; + if (value == 2) { + textList = s1.textList; + } + }); + } + } + + void clearMeeting() { + meetingId = 0; + _wTasktype?.dispose(); + _wTasktype = null; + } + + void _rebuild() { + final Set ids = {}; + for (var e in s1.taskFinishList) { + if (e is Map && e['id'] is int) ids.add(e['id'] as int); + } + for (var e in s2.taskFinishList) { + if (e is Map && e['id'] is int) ids.add(e['id'] as int); + } + final List unified = []; + for (final id in ids) { + final String svc = + (_storage.read("meeting_service_type_$id") ?? '').toString(); + final Map a = s1.taskFinishList.firstWhere( + (x) => x['id'] == id, + orElse: () => {}, + ); + final Map b = s2.taskFinishList.firstWhere( + (x) => x['id'] == id, + orElse: () => {}, + ); + int va = a.isNotEmpty ? ((a['tasktype'] ?? 0) as int) : -999; + int vb = b.isNotEmpty ? ((b['tasktype'] ?? 0) as int) : -999; + int v; + if (svc == 'fast') { + if (va != -999 && vb != -999) { + v = va >= vb ? va : vb; + } else { + v = vb != -999 ? vb : va; + } + } else if (svc == 'standard') { + if (va != -999 && vb != -999) { + v = va >= vb ? va : vb; + } else { + v = va != -999 ? va : vb; + } + } else { + if (va == -999 && vb == -999) { + continue; + } + v = va == -999 ? vb : (vb == -999 ? va : (va >= vb ? va : vb)); + } + unified.add({'id': id, 'tasktype': v}); + } + taskFinishList.value = unified; + } +} diff --git a/lib/data/services/meeting/meeting_task_service.dart b/lib/data/services/meeting/meeting_task_service.dart index 5688628b8..3d7886b19 100644 --- a/lib/data/services/meeting/meeting_task_service.dart +++ b/lib/data/services/meeting/meeting_task_service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; @@ -6,16 +7,20 @@ import 'package:http/http.dart' as http; import '../../../core/utils/logger.dart'; import '../../../core/utils/synchrodata.dart'; +import '../../models/meeting_usage_stats.dart'; import '../db/sqflite_api.dart'; -import '../deapsound_ai_service.dart'; +import '../usage_stats_service.dart'; import 'meeting_upload_service.dart'; import '../../../data/models/appconfig.dart'; +import 'summary_task_service.dart'; ///会议任务服务类,负责管理会议音频的转写任务处理流程,包括任务提交、状态轮询、结果处理和AI分析等功能。 +///火山引擎录音文件识别大模型-标准版 class MeetingTaskService extends GetxService { final GetStorage _storage = GetStorage(); MeetingUploadService get _uploadService => Get.find(); + SummaryTaskService get _summaryTaskService => Get.find(); List _taskList = []; //待处理的会议任务列表 RxList taskFinishList = [].obs; //已处理的会议任务列表 @@ -45,8 +50,6 @@ class MeetingTaskService extends GetxService { int meetingId = 0; RxInt tasktype = 1.obs; //任务类型,1-转写中,2-总结中,3-完成 List textList = []; //转写结果列表 - String summary = ''; //AI总结内容 - String overview = ''; //AI概览内容 @override void onInit() { @@ -58,6 +61,15 @@ class MeetingTaskService extends GetxService { _getTaskList(); } + void cancelTask() { + _isTask = false; + _taskList.clear(); + _storage.remove("meeting_task_list"); + _storage.remove("meeting_task_finish_list"); + _uploadService.cancelUpload(); + _summaryTaskService.cancelTask(); + } + Future _initialize() async { try { _appid = AppConfig.env('VOLC_OPENSPEECH_APP_ID') ?? ''; @@ -153,6 +165,37 @@ class MeetingTaskService extends GetxService { taskId = await _submitTask(audiourl, isSpeaker, language); } await SqfliteApi.editMeeting(id, {'tasktype': 1, 'taskid': taskId}); + try { + final detail = await SqfliteApi.getMeetingDetails(id); + final seconds = (detail['seconds'] ?? 0) as int; + final filepath = (detail['filepath'] ?? '') as String; + int fileSizeBytes = 0; + if (filepath.isNotEmpty) { + try { + fileSizeBytes = await File(filepath).length(); + } catch (_) {} + } + final record = MeetingAsrRecord( + id: taskId, + meetingId: id, + taskId: taskId, + audioSeconds: seconds, + fileSizeBytes: fileSizeBytes, + language: language, + submitTimestamp: DateTime.now().millisecondsSinceEpoch, + status: asrStatusProcessing, + transcriptBytes: 0, + completeTimestamp: 0, + ); + try { + final svc = Get.isRegistered() + ? Get.find() + : Get.put(UsageStatsService(), permanent: true); + svc.addMeetingAsrRecord(record); + } catch (e) { + Logger.w('MeetingASRrecord', '提交用量记录失败: ' + e.toString()); + } + } catch (_) {} _taskList.add({ 'id': id, 'taskid': taskId, @@ -197,6 +240,7 @@ class MeetingTaskService extends GetxService { if (isDeleteTask) { _taskList.removeWhere((element) => element['id'] == id); _storage.write("meeting_task_list", _taskList); + _summaryTaskService.removeTask(id); } } @@ -332,7 +376,8 @@ class MeetingTaskService extends GetxService { Logger.info("lxm---大模型asr响应体结果$respDic['result']['text']"); - if (respDic['result']['text'] != null) { + if (respDic['result']['text'] != null && + respDic['result']['text'] != '') { await _addTextList( item['id'], //会议ID respDic['result']['utterances'], //语音转文字结果 @@ -409,77 +454,25 @@ class MeetingTaskService extends GetxService { _storage.write("meeting_task_list", _taskList); _storage.write("meeting_task_finish_list", taskFinishList); } - - // 触发AI分析,分别请求总结和概览 - aiServiceMessage(id, text, prompt, outline); - } - - ///触发AI分析,分别请求总结和概览 - ///[id] 会议ID - ///[text] 语音转文字结果 - ///[prompt] AI总结提示词 - ///[outline] AI概览提示词 - void aiServiceMessage(int id, String text, String prompt, String outline) { - int awaitQuantity = 0; - String summaryText = ''; - String overviewText = ''; - DeapsoundAIService().sendMessage( - messages: [ - { - 'role': 'system', - 'content': prompt, - }, - {'role': 'user', 'content': text}, - ], - ).then((value) { - awaitQuantity++; - summaryText = value; - Logger.info("lxm---大模型asr总结结果$summaryText"); - if (awaitQuantity >= 2) { - _aiServiceValue(id, summaryText, overviewText); - } - }); - DeapsoundAIService().sendMessage( - messages: [ - { - 'role': 'system', - 'content': outline, - }, - {'role': 'user', 'content': text}, - ], - ).then((value) { - awaitQuantity++; - overviewText = value; - Logger.info("lxm---大模型asr概览结果$overviewText"); - if (awaitQuantity >= 2) { - _aiServiceValue(id, summaryText, overviewText); + try { + final details = await SqfliteApi.getMeetingDetails(id); + final tid = (details['taskid'] ?? '') as String; + final transcriptBytes = utf8.encode(text).length; + try { + final svc = Get.isRegistered() + ? Get.find() + : Get.put(UsageStatsService(), permanent: true); + svc.completeMeetingAsrRecord( + id: tid, + transcriptBytes: transcriptBytes, + completeTimestamp: DateTime.now().millisecondsSinceEpoch, + ); + } catch (e) { + Logger.w('MeetingASRrecord', '完成用量记录失败: ' + e.toString()); } - }); - } + } catch (_) {} - ///处理AI分析完成后的结果,包括数据库存储和状态更新 - ///[id] 会议ID - ///[summaryText] 总结结果 - ///[overviewText] 概览结果 - void _aiServiceValue(int id, String summaryText, String overviewText) { - SqfliteApi.editMeeting(id, { - 'tasktype': 3, - 'summary': summaryText, - 'overview': overviewText, - }); - if (id == meetingId) { - summary = summaryText; - overview = overviewText; - tasktype.value = 3; - } - Map taskFinishData = taskFinishList.firstWhere( - (element) => element['id'] == id, - orElse: () => {}, - ); - if (taskFinishData.isNotEmpty) { - taskFinishData['tasktype'] = 3; - taskFinishList.refresh(); - } - _storage.write("meeting_task_finish_list", taskFinishList); + // 触发AI分析,分别请求总结和概览 + _summaryTaskService.addTaskList(id, text, prompt, outline); } } diff --git a/lib/data/services/meeting/meeting_task_service2.dart b/lib/data/services/meeting/meeting_task_service2.dart new file mode 100644 index 000000000..ff69e20ff --- /dev/null +++ b/lib/data/services/meeting/meeting_task_service2.dart @@ -0,0 +1,971 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart' as http; + +import '../db/sqflite_api.dart'; +import '../../../data/models/appconfig.dart'; +import '../../../core/utils/logger.dart'; +import '../../../core/utils/synchrodata.dart'; +import '../usage_stats_service.dart'; +import '../../../data/models/meeting_usage_stats.dart'; +import '../deapsound_ai_service.dart'; +import 'meeting_upload_service.dart'; +// 移除标准版服务桥接,改为通过聚合器对外暴露统一状态 +import 'summary_task_service.dart'; + +//火山引擎录音文件识别大模型-极速版 +class MeetingTaskService2 extends GetxService { + final GetStorage _storage = GetStorage(); + + MeetingUploadService get _uploadService => Get.find(); + SummaryTaskService get _summaryTaskService => Get.find(); + + List _taskList = []; + RxList taskFinishList = [].obs; + + bool _isTask = false; + + String _systemPrompt = ''; + String _userId = ''; + + late final String _appid; + late final String _token; + late final String _serviceUrl; + + int meetingId = 0; + RxInt tasktype = 1.obs; + List textList = []; + String summary = ''; + String overview = ''; + + @override + void onInit() { + super.onInit(); + _getAgent(); + _getTaskList(); + } + + void _getAgent() async { + Map userInfo = _storage.read("user_info"); + _userId = userInfo['user']['uid']; + _systemPrompt = AppConfig.isInitialized() + ? (AppConfig.agentsystemPrompt("meet_assistant") ?? '') + : ''; + final String base = AppConfig.env('VOLC_OPENSPEECH_BASE_URL_BIGMODEL') ?? + 'https://openspeech.bytedance.com/api/v3/auc/bigmodel'; + _appid = AppConfig.env('VOLC_OPENSPEECH_APP_ID') ?? ''; + _token = AppConfig.env('VOLC_OPENSPEECH_ACCESS_TOKEN') ?? ''; + _serviceUrl = + base.endsWith('/recognize/flash') ? base : (base + '/recognize/flash'); + } + + void _getTaskList() { + _taskList = _storage.read("meeting_task_list_v2") ?? []; + taskFinishList.value = _storage.read("meeting_task_finish_list_v2") ?? []; + _executeTask(); + } + + Future addTask( + int id, + String audiourl, { + bool isSpeaker = false, + String language = '', + String targetLanguage = '', + String prompt = '', + String outline = '', + }) async { + if (audiourl.isEmpty) { + Map uploadData = _uploadService.uploadList.firstWhere( + (element) => element['id'] == id, + orElse: () => {}, + ); + if (uploadData.isNotEmpty) { + if (uploadData['audiourl'].isEmpty) { + uploadData['isTask'] = true; + uploadData['template'] = { + 'isSpeaker': isSpeaker, + 'language': language, + 'targetLanguage': targetLanguage, + 'prompt': prompt, + 'outline': outline, + }; + _storage.write("meeting_upload_list", _uploadService.uploadList); + } else { + await addTaskList( + id, + uploadData['audiourl'], + isSpeaker: isSpeaker, + language: language, + targetLanguage: targetLanguage, + prompt: prompt, + outline: outline, + ); + } + } + } else { + await addTaskList( + id, + audiourl, + isSpeaker: isSpeaker, + language: language, + targetLanguage: targetLanguage, + prompt: prompt, + outline: outline, + ); + } + } + + Future addTaskList( + int id, + String audiourl, { + bool isSpeaker = false, + String language = '', + String targetLanguage = '', + String prompt = '', + String outline = '', + }) async { + final String taskId = DateTime.now().millisecondsSinceEpoch.toString(); + await SqfliteApi.editMeeting(id, {'tasktype': 1, 'taskid': taskId}); + + try { + final detail = await SqfliteApi.getMeetingDetails(id); + final seconds = (detail['seconds'] ?? 0) as int; + final filepath = (detail['filepath'] ?? '') as String; + int fileSizeBytes = 0; + if (filepath.isNotEmpty) { + try { + fileSizeBytes = await File(filepath).length(); + } catch (_) {} + } + final record = MeetingAsrRecord( + id: taskId, + meetingId: id, + taskId: taskId, + audioSeconds: seconds, + fileSizeBytes: fileSizeBytes, + language: language, + submitTimestamp: DateTime.now().millisecondsSinceEpoch, + status: asrStatusProcessing, + transcriptBytes: 0, + completeTimestamp: 0, + ); + try { + final svc = Get.isRegistered() + ? Get.find() + : Get.put(UsageStatsService(), permanent: true); + svc.addMeetingAsrRecord(record); + } catch (e) { + Logger.w('MeetingASRrecord', '提交用量记录失败: ' + e.toString()); + } + } catch (_) {} + _taskList.add({ + 'id': id, + 'audiourl': audiourl, + 'tasktype': 1, + 'taskid': taskId, + 'template': { + 'isSpeaker': isSpeaker, + 'language': language, + 'targetLanguage': targetLanguage, + 'prompt': prompt, + 'outline': outline, + }, + }); + taskFinishList.add({'id': id, 'tasktype': 1}); + _storage.write("meeting_task_list_v2", _taskList); + _storage.write("meeting_task_finish_list_v2", taskFinishList); + // 由聚合器统一对外呈现状态,不直接写入标准版列表 + _executeTask(); + } + + void _executeTask() async { + if (_isTask) return; + _isTask = true; + Logger.debug('开始执行任务,任务数量:${_taskList.length}'); + + while (_taskList.isNotEmpty) { + Logger.debug('任务循环执行,剩余任务:${_taskList.length}'); + await Future.delayed(const Duration(seconds: 2)); + + // 创建任务列表的副本,避免在迭代过程中修改原列表 + List tasksCopy = List.from(_taskList); + + for (var element in tasksCopy) { + Logger.debug('处理任务:${element['id']},音频:${element['audiourl']}'); + + // 检查任务是否仍在列表中(可能已被其他地方移除) + if (_taskList.any((task) => task['id'] == element['id'])) { + try { + await _fileRecognize(element); + } catch (e) { + Logger.error('处理任务失败:${element['id']},错误:$e'); + // 确保失败的任务被移除,避免无限循环 + _taskList.removeWhere((task) => task['id'] == element['id']); + _storage.write("meeting_task_list_v2", _taskList); + + // 更新任务状态为失败 + await SqfliteApi.editMeeting(element['id'], {'tasktype': 0}); + } + } else { + Logger.debug('任务 ${element['id']} 已从任务列表移除'); + } + } + + // 如果没有任务被处理,避免无限循环 + if (_taskList.length == tasksCopy.length) { + Logger.warning('本次循环未处理任何任务,为避免无限执行终止循环'); + break; + } + } + + _isTask = false; + Logger.debug('任务执行完成,剩余任务:${_taskList.length}'); + } + + void removeTask(int id, {bool isDeleteTask = false}) { + Map taskFinishData = taskFinishList.firstWhere( + (element) => element['id'] == id, + orElse: () => {}, + ); + if (taskFinishData.isNotEmpty) { + if (isDeleteTask || taskFinishData['tasktype'] >= 3) { + taskFinishList.remove(taskFinishData); + _storage.write("meeting_task_finish_list_v2", taskFinishList); + } + } + if (isDeleteTask) { + _taskList.removeWhere((element) => element['id'] == id); + _storage.write("meeting_task_list_v2", _taskList); + } + } + + Future> _recognizeAudio( + String audiourl, { + bool enableSpeakerInfo = true, + String language = '', + String? requestIdOverride, + }) async { + String requestId = + requestIdOverride ?? DateTime.now().millisecondsSinceEpoch.toString(); + + // 为每次请求生成新的headers - 适配大模型极速版API + Map requestHeaders = { + 'X-Api-App-Key': _appid, + 'X-Api-Access-Key': _token, + 'X-Api-Resource-Id': 'volc.bigasr.auc_turbo', + 'X-Api-Request-Id': requestId, + 'X-Api-Sequence': '-1', + 'Content-Type': 'application/json', + }; + + // 使用大模型极速版API的新请求格式 + final Map requestPayload = { + "user": {"uid": _appid}, + "audio": {"url": audiourl}, + "request": { + "model_name": "bigmodel", + "enable_itn": true, + "enable_punc": true, + "enable_ddc": true, + "enable_speaker_info": enableSpeakerInfo, + "word_info": 1, + "show_utterances": true + } + }; + + if (language.isNotEmpty) { + requestPayload['request']['language'] = language; + } + + final String requestBody = jsonEncode(requestPayload); + + try { + // 记录请求详情 + Logger.debug('API 请求地址:$_serviceUrl'); + Logger.debug('API 请求头:$requestHeaders'); + Logger.debug('API 请求体:$requestBody'); + + final response = await http.post( + Uri.parse(_serviceUrl), + headers: requestHeaders, + body: requestBody, + ); + + Logger.debug('API 响应状态码:${response.statusCode}'); + Logger.debug('API 响应头:${response.headers}'); + + if (response.statusCode != 200) { + Logger.error('识别失败,状态码:${response.statusCode},响应体:${response.body}'); + throw Exception('识别失败,状态码:${response.statusCode}'); + } + + final respDic = jsonDecode(response.body); + Logger.debug('识别接口响应:$respDic'); + + // 检查响应中是否包含资源相关信息 + if (respDic.containsKey('resource_id') || respDic.containsKey('model')) { + Logger.debug('资源校验 - 使用的资源ID:volc.bigasr.auc_turbo'); + Logger.debug( + '响应中的资源信息:${respDic['resource_id'] ?? respDic['model'] ?? '响应未包含'}'); + } + + return respDic; + } catch (e) { + Logger.error('识别请求异常:${e.toString()}'); + rethrow; + } + } + + String _getFileType(String path) { + List array = path.split('.'); + return array[array.length - 1]; + } + + int _parseTime(dynamic timeValue) { + if (timeValue == null) return 0; + + if (timeValue is int) { + return timeValue; + } else if (timeValue is double) { + return timeValue.round(); + } else if (timeValue is String) { + try { + double parsed = double.parse(timeValue); + return parsed.round(); + } catch (e) { + Logger.error('时间值解析失败:$timeValue'); + return 0; + } + } else { + Logger.error('未知的时间值类型:${timeValue.runtimeType},值:$timeValue'); + return 0; + } + } + + Future _fileRecognize(Map item) async { + try { + Logger.debug('开始识别音频:${item['audiourl']}'); + // 识别语言选取:若设置了目标输出语言且与录音语言一致,优先让ASR自动识别,避免错配 + final String tplLang = + (((item['template'] ?? {})['language']) ?? '').toString(); + final String tplTarget = + (((item['template'] ?? {})['targetLanguage']) ?? '').toString(); + String asrLanguage = tplLang; + if (tplTarget.isNotEmpty && tplTarget == tplLang) { + asrLanguage = ''; + } + var respDic = await _recognizeAudio( + item['audiourl'], + enableSpeakerInfo: + (((item['template'] ?? {})['isSpeaker']) ?? false) as bool, + language: asrLanguage, + requestIdOverride: (item['taskid'] ?? '').toString().isEmpty + ? null + : (item['taskid'] ?? '').toString(), + ); + + Logger.debug('API 全量响应结构:${respDic.toString()}'); + Logger.debug('响应包含的键:${respDic.keys.toList()}'); + + // 检查响应格式并提取utterances + List utterances = []; + + // 检查不同可能的响应格式 + if (respDic['result'] != null && + respDic['result']['utterances'] != null) { + utterances = respDic['result']['utterances']; + Logger.debug('在 result.utterances 中找到 ${utterances.length} 条语句'); + } else if (respDic['resp'] != null && + respDic['resp']['utterances'] != null) { + utterances = respDic['resp']['utterances']; + Logger.debug('在 resp.utterances 中找到 ${utterances.length} 条语句'); + } else if (respDic['utterances'] != null) { + utterances = respDic['utterances']; + Logger.debug('在根级 utterances 中找到 ${utterances.length} 条语句'); + } else if (respDic['data'] != null && + respDic['data']['utterances'] != null) { + utterances = respDic['data']['utterances']; + Logger.debug('在 data.utterances 中找到 ${utterances.length} 条语句'); + } else if (respDic['response'] != null && + respDic['response']['utterances'] != null) { + utterances = respDic['response']['utterances']; + Logger.debug('在 response.utterances 中找到 ${utterances.length} 条语句'); + } else { + // 检查是否有错误信息 + if (respDic['error'] != null) { + Logger.error('接口返回错误:${respDic['error']}'); + } else if (respDic['message'] != null) { + Logger.error('接口返回消息:${respDic['message']}'); + } else { + Logger.error('未知的响应格式。可用键:${respDic.keys.toList()}'); + // 尝试查找任何包含文本的字段 + respDic.forEach((key, value) { + Logger.debug('键:$key,值类型:${value.runtimeType},值:$value'); + }); + } + } + + if (utterances.isNotEmpty) { + Logger.debug('开始处理 ${utterances.length} 条语句'); + await _addTextList(item['id'], utterances, item['template']); + // 从任务列表中移除已完成的任务 + _taskList.removeWhere((element) => element['id'] == item['id']); + _storage.write("meeting_task_list_v2", _taskList); + Logger.debug('音频识别成功,会议ID:${item['id']}'); + + // 更新UI状态 - 如果是当前会议,触发UI更新 + if (item['id'] == meetingId) { + tasktype.value = 2; // 转写完成,进入分析阶段 + } + } else { + Logger.error('响应中未发现语句。完整响应:$respDic'); + Logger.debug('未检测到语句,开始执行无声音频检查'); + Logger.debug('无声音频检测调试信息'); + Logger.debug('响应键列表:${respDic.keys.toList()}'); + Logger.debug('响应代码:${respDic['code']}'); + Logger.debug('响应消息:${respDic['message']}'); + + // 检查是否为无声音频 + bool isSilentAudio = _checkIfSilentAudio(respDic); + Logger.debug('无声检测结果:$isSilentAudio'); + + if (isSilentAudio) { + Logger.debug('检测到无声音频,设置任务类型为 -1'); + // 设置无声音频状态 (使用特殊值 -1) + await SqfliteApi.editMeeting(item['id'], {'tasktype': -1}); + if (item['id'] == meetingId) { + Logger.debug('当前会议设置 tasktype.value 为 -1'); + tasktype.value = -1; // 无声音频 + } + + // 更新taskFinishList中的状态 + Map taskFinishData = taskFinishList.firstWhere( + (element) => element['id'] == item['id'], + orElse: () => {}, + ); + if (taskFinishData.isNotEmpty) { + taskFinishData['tasktype'] = -1; + taskFinishList.refresh(); + } + _storage.write("meeting_task_finish_list_v2", taskFinishList); + // 由聚合器统一对外呈现状态,不直接写入标准版列表 + // 由聚合器统一对外呈现状态,不直接写入标准版列表 + } else { + Logger.error('识别失败,原因未知'); + // 识别失败,更新数据库状态为失败 + await SqfliteApi.editMeeting(item['id'], {'tasktype': 0}); + if (item['id'] == meetingId) { + tasktype.value = 0; // 识别失败 + } + + // 更新taskFinishList中的状态 + Map taskFinishData = taskFinishList.firstWhere( + (element) => element['id'] == item['id'], + orElse: () => {}, + ); + if (taskFinishData.isNotEmpty) { + taskFinishData['tasktype'] = 0; + taskFinishList.refresh(); + } + _storage.write("meeting_task_finish_list_v2", taskFinishList); + } + + // 从任务列表中移除 + _taskList.removeWhere((element) => element['id'] == item['id']); + _storage.write("meeting_task_list_v2", _taskList); + } + } catch (e) { + Logger.error('文件识别流程发生异常:${e.toString()}'); + Logger.error('堆栈信息:${StackTrace.current}'); + + // 更新数据库状态为失败 + try { + await SqfliteApi.editMeeting(item['id'], {'tasktype': 0}); + } catch (dbError) { + Logger.error('数据库更新会议状态失败:$dbError'); + } + + // 出错时也要从任务列表中移除,避免无限重试 + _taskList.removeWhere((element) => element['id'] == item['id']); + _storage.write("meeting_task_list_v2", _taskList); + + // 更新UI状态 - 如果是当前会议,显示错误状态 + if (item['id'] == meetingId) { + tasktype.value = 0; // 识别失败 + } + } + } + + /// 检查是否为无声音频 + bool _checkIfSilentAudio(Map response) { + Logger.debug('检查是否为无声音频,响应:${response.toString()}'); + + // 检查API响应中的错误信息或状态码 + // 根据实际API响应格式调整检测逻辑 + + // 检查是否有错误码表示无声音频 + if (response['code'] != null) { + int code = response['code']; + Logger.debug('检测到响应代码:$code'); + // 常见的无声音频错误码 + if (code == 40001 || + code == 40002 || + code == 40003 || + code == 50001 || + code == 50002 || + code == 50003) { + Logger.debug('根据错误码判断为无声音频:$code'); + return true; + } + } + + // 检查错误消息 + if (response['message'] != null) { + String message = response['message'].toString().toLowerCase(); + Logger.debug('检测到响应消息:$message'); + if (message.contains('silent') || + message.contains('no audio') || + message.contains('无声') || + message.contains('静音') || + message.contains('empty audio') || + message.contains('no speech') || + message.contains('audio too short') || + message.contains('insufficient audio')) { + Logger.debug('根据消息判断为无声音频:$message'); + return true; + } + } + + // 检查result中的错误信息 + if (response['result'] != null && response['result']['error'] != null) { + String error = response['result']['error'].toString().toLowerCase(); + Logger.debug('检测到 result.error:$error'); + if (error.contains('silent') || + error.contains('no audio') || + error.contains('无声') || + error.contains('静音') || + error.contains('empty audio') || + error.contains('no speech') || + error.contains('audio too short')) { + Logger.debug('根据 result.error 判断为无声音频:$error'); + return true; + } + } + + // 检查是否有音频时长信息且时长为0或极短 + if (response['result'] != null && response['result']['duration'] != null) { + double duration = (response['result']['duration'] as num).toDouble(); + Logger.debug('检测到音频时长:$duration 秒'); + if (duration <= 0.5) { + // 音频时长小于0.5秒认为是无声 + Logger.debug('根据时长过短判断为无声音频:$duration'); + return true; + } + } + + // 检查utterances是否为空且没有其他明确的成功标识 + bool hasUtterances = false; + if (response['result'] != null && + response['result']['utterances'] != null) { + hasUtterances = (response['result']['utterances'] as List).isNotEmpty; + } else if (response['resp'] != null && + response['resp']['utterances'] != null) { + hasUtterances = (response['resp']['utterances'] as List).isNotEmpty; + } else if (response['utterances'] != null) { + hasUtterances = (response['utterances'] as List).isNotEmpty; + } else if (response['data'] != null && + response['data']['utterances'] != null) { + hasUtterances = (response['data']['utterances'] as List).isNotEmpty; + } else if (response['response'] != null && + response['response']['utterances'] != null) { + hasUtterances = (response['response']['utterances'] as List).isNotEmpty; + } + + // 检查文本内容是否为空 + String textContent = ''; + if (response['result'] != null && response['result']['text'] != null) { + textContent = response['result']['text'].toString().trim(); + } else if (response['text'] != null) { + textContent = response['text'].toString().trim(); + } else if (response['data'] != null && response['data']['text'] != null) { + textContent = response['data']['text'].toString().trim(); + } + + // 如果文本内容为空且音频时长大于1秒,很可能是无声音频 + if (textContent.isEmpty) { + double audioDuration = 0; + if (response['audio_info'] != null && + response['audio_info']['duration'] != null) { + audioDuration = (response['audio_info']['duration'] as num).toDouble() / + 1000; // 转换为秒 + } else if (response['result'] != null && + response['result']['additions'] != null && + response['result']['additions']['duration'] != null) { + audioDuration = + (response['result']['additions']['duration'] as num).toDouble() / + 1000; // 转换为秒 + } + + Logger.debug('文本内容为空,音频时长:$audioDuration 秒'); + if (audioDuration > 1.0) { + // 音频时长大于1秒但没有文本内容,认为是无声音频 + Logger.debug('判断为无声音频:文本为空且时长充足'); + return true; + } + } + + // 如果没有utterances且响应状态正常,可能是无声音频 + if (!hasUtterances) { + // 检查是否有明确的成功状态码 + bool hasSuccessCode = false; + if (response['code'] != null) { + int code = response['code']; + hasSuccessCode = (code == 0 || code == 200); + } + + // 如果有成功状态码但没有utterances,很可能是无声音频 + if (hasSuccessCode) { + Logger.debug('判断为无声音频:成功状态码但无语句'); + return true; + } + } + + Logger.debug('未判定为无声音频'); + return false; + } + + Future _addTextList(int id, List utterances, + [Map? templateData]) async { + Logger.debug('处理会议 $id 的 ${utterances.length} 条语句'); + String text = ''; + String personnel = ''; + List> list = []; + + for (int i = 0; i < utterances.length; i++) { + var element = utterances[i]; + Logger.debug('处理第 $i 条语句:${element.toString()}'); + + String speaker = ''; + String content = ''; + int startTime = 0; + int endTime = 0; + + // 提取说话人信息 - 检查多种可能的字段名 + if (element['additions'] != null && element['additions'].isNotEmpty) { + if (element['additions']['speaker'] != null) { + speaker = 'Speaker ${element['additions']['speaker']}'; + } + } else if (element['speaker'] != null) { + speaker = 'Speaker ${element['speaker']}'; + } else if (element['speaker_id'] != null) { + speaker = 'Speaker ${element['speaker_id']}'; + } + + // 提取文本内容 - 检查多种可能的字段名 + if (element['text'] != null) { + content = element['text'].toString(); + } else if (element['content'] != null) { + content = element['content'].toString(); + } else if (element['transcript'] != null) { + content = element['transcript'].toString(); + } else { + Logger.error('语句中未找到文本内容:$element'); + continue; + } + + // 提取时间信息 - 检查多种可能的字段名和格式 + if (element['start_time'] != null) { + startTime = _parseTime(element['start_time']); + } else if (element['startTime'] != null) { + startTime = _parseTime(element['startTime']); + } else if (element['begin'] != null) { + startTime = _parseTime(element['begin']); + } + + if (element['end_time'] != null) { + endTime = _parseTime(element['end_time']); + } else if (element['endTime'] != null) { + endTime = _parseTime(element['endTime']); + } else if (element['end'] != null) { + endTime = _parseTime(element['end']); + } + + if (speaker.isNotEmpty) { + text += '[$speaker]: '; + if (!personnel.contains(speaker)) { + personnel += '[$speaker] '; + } + } + + list.add({ + 'meetingid': id, + 'content': content, + 'starttime': startTime, + 'endtime': endTime, + 'speaker': speaker, + }); + text += '$content\n'; + + Logger.debug('已添加语句:说话人=$speaker,内容=$content,开始=$startTime,结束=$endTime'); + } + + Logger.debug('处理完成的语句总数:${list.length}'); + var result = await SqfliteApi.insertMeetingSpeaker(list); + if (result != null) { + // 初始用于总结/概览的文本,先用原始拼接文本作为默认值 + String summarySourceText = text; + // 根据目标语言决定是否需要翻译文本列表 + final String targetLanguage = + ((templateData?['targetLanguage']) ?? '').toString(); + final String sourceLanguage = + ((templateData?['language']) ?? '').toString(); + bool needTranslate = false; + if (targetLanguage.isNotEmpty) { + // 若未指定录音语言,默认认为需要翻译;否则当目标语言与录音语言主码不同则翻译 + if (sourceLanguage.isEmpty) { + needTranslate = true; + } else { + String srcPrefix = sourceLanguage.toLowerCase().substring(0, 2); + String tgtPrefix = targetLanguage.toLowerCase().substring(0, 2); + needTranslate = srcPrefix != tgtPrefix; + } + } + if (needTranslate) { + try { + // 保持在“正在转写”状态,先进行翻译为目标语言 + await _translateTextList(id, targetLanguage); + } catch (e) { + Logger.error('转写文本翻译失败:${e.toString()}'); + } + // 翻译完成后再写入 personnel、设置已完成并刷新 UI + await SqfliteApi.editMeeting( + id, {'tasktype': 2, 'personnel': personnel}); + List speakerList = await SqfliteApi.getMeetingSpeaker(id); + List newSpeakerList = List.from(speakerList); + Synchrodata.saveOperationRecord('update', { + 'original': jsonEncode(newSpeakerList), + 'id': id, + }); + if (id == meetingId) { + textList = newSpeakerList; + tasktype.value = 2; + } + // 由聚合器统一对外呈现状态,不直接写入标准版列表 + // 由聚合器统一对外呈现状态,不直接写入标准版列表 + // 用最新(已翻译)的文本重建总结/概览输入 + summarySourceText = ''; + for (var element in newSpeakerList) { + final String spk = (element['speaker'] ?? '').toString(); + final String cnt = (element['content'] ?? '').toString(); + if (spk.isNotEmpty) { + summarySourceText += '[$spk]: '; + } + summarySourceText += '$cnt\n'; + } + Map taskFinishData = taskFinishList.firstWhere( + (element) => element['id'] == id, + orElse: () => {}, + ); + if (taskFinishData.isNotEmpty) { + taskFinishData['tasktype'] = 2; + taskFinishList.refresh(); + } + _storage.write("meeting_task_finish_list_v2", taskFinishList); + } else { + // 原有中文直出流程 + await SqfliteApi.editMeeting( + id, {'tasktype': 2, 'personnel': personnel}); + List speakerList = await SqfliteApi.getMeetingSpeaker(id); + List newSpeakerList = List.from(speakerList); + Synchrodata.saveOperationRecord('update', { + 'original': jsonEncode(newSpeakerList), + 'id': id, + }); + if (id == meetingId) { + textList = newSpeakerList; + tasktype.value = 2; + } + // 仍使用最新DB中的文本构建总结/概览输入,避免与原始拼接文本不一致 + summarySourceText = ''; + for (var element in newSpeakerList) { + final String spk = (element['speaker'] ?? '').toString(); + final String cnt = (element['content'] ?? '').toString(); + if (spk.isNotEmpty) { + summarySourceText += '[$spk]: '; + } + summarySourceText += '$cnt\n'; + } + Map taskFinishData = taskFinishList.firstWhere( + (element) => element['id'] == id, + orElse: () => {}, + ); + if (taskFinishData.isNotEmpty) { + taskFinishData['tasktype'] = 2; + taskFinishList.refresh(); + } + _storage.write("meeting_task_finish_list_v2", taskFinishList); + } + // 使用统一的总结服务生成概览与总结 + final String tplPrompt = ((templateData?['prompt']) ?? '').toString(); + final String tplOutline = ((templateData?['outline']) ?? '').toString(); + final String outCode = (((templateData?['targetLanguage']) ?? + (templateData?['language']) ?? + '') as String) + .toLowerCase(); + String langDesc = ''; + if (outCode.startsWith('zh')) { + langDesc = '中文'; + } else if (outCode.startsWith('en')) { + langDesc = '英文'; + } else if (outCode.startsWith('ja')) { + langDesc = '日文'; + } else if (outCode.startsWith('ko')) { + langDesc = '韩文'; + } else if (outCode.startsWith('fr')) { + langDesc = '法文'; + } else if (outCode.startsWith('de')) { + langDesc = '德文'; + } else if (outCode.startsWith('es')) { + langDesc = '西班牙文'; + } else if (outCode.startsWith('ru')) { + langDesc = '俄文'; + } else if (outCode.startsWith('it')) { + langDesc = '意大利文'; + } else if (outCode.startsWith('pt')) { + langDesc = '葡萄牙文'; + } else if (outCode.startsWith('th')) { + langDesc = '泰文'; + } else if (outCode.startsWith('vi')) { + langDesc = '越南文'; + } + final String promptToUse = tplPrompt.isEmpty + ? (langDesc.isEmpty + ? '' + : '输出语言:' + langDesc + '。请全程使用' + langDesc + '输出。') + : (langDesc.isEmpty + ? tplPrompt + : tplPrompt + ' 输出语言:' + langDesc + '。请全程使用' + langDesc + '输出。'); + final String outlineToUse = tplOutline.isEmpty + ? (langDesc.isEmpty + ? '' + : '输出语言:' + langDesc + '。请全程使用' + langDesc + '输出。') + : (langDesc.isEmpty + ? tplOutline + : tplOutline + ' 输出语言:' + langDesc + '。请全程使用' + langDesc + '输出。'); + try { + final details = await SqfliteApi.getMeetingDetails(id); + final tid = (details['taskid'] ?? '') as String; + final transcriptBytes = utf8.encode(summarySourceText).length; + final svc = Get.isRegistered() + ? Get.find() + : Get.put(UsageStatsService(), permanent: true); + svc.completeMeetingAsrRecord( + id: tid, + transcriptBytes: transcriptBytes, + completeTimestamp: DateTime.now().millisecondsSinceEpoch, + ); + } catch (e) { + Logger.w('MeetingASRrecord', '完成用量记录失败: ' + e.toString()); + } + + _summaryTaskService.addTaskList( + id, summarySourceText, promptToUse, outlineToUse); + } + } + + Future _translateTextList(int id, String targetLanguage) async { + // 读取刚插入的文本条目 + List speakerList = await SqfliteApi.getMeetingSpeaker(id); + // 构造简洁的可翻译列表 + List toTranslate = speakerList + .map((e) => { + 'id': e['id'], + 'speaker': (e['speaker'] ?? '').toString(), + 'content': (e['content'] ?? '').toString(), + }) + .toList(); + + final String userJson = jsonEncode({ + 'targetLanguage': targetLanguage, + 'items': toTranslate, + }); + + // 根据目标语言构建提示语 + String tgtName = targetLanguage.toLowerCase(); + String langDesc = '目标语言'; + if (tgtName.startsWith('zh')) { + langDesc = '中文'; + } else if (tgtName.startsWith('en')) { + langDesc = '英文'; + } else if (tgtName.startsWith('ja')) { + langDesc = '日文'; + } else if (tgtName.startsWith('ko')) { + langDesc = '韩文'; + } else if (tgtName.startsWith('fr')) { + langDesc = '法文'; + } else if (tgtName.startsWith('de')) { + langDesc = '德文'; + } else if (tgtName.startsWith('es')) { + langDesc = '西班牙文'; + } else if (tgtName.startsWith('ru')) { + langDesc = '俄文'; + } else if (tgtName.startsWith('it')) { + langDesc = '意大利文'; + } else if (tgtName.startsWith('pt')) { + langDesc = '葡萄牙文'; + } else if (tgtName.startsWith('th')) { + langDesc = '泰文'; + } else if (tgtName.startsWith('vi')) { + langDesc = '越南文'; + } + + final String systemPrompt = '你是专业的翻译助手。请将每条items中的content精准翻译为' + + langDesc + + ',保持术语专业、语气自然。仅返回JSON数组,元素形如{"id":<原id>,"content":"<翻译后文本>"},不要额外说明,不要漏掉任何一条。'; + + final String? translated = await DeapsoundAIService().sendMessage( + messages: [ + {'role': 'system', 'content': systemPrompt}, + {'role': 'user', 'content': userJson}, + ], + ); + + if (translated == null || translated.isEmpty) return; + List arr; + try { + arr = jsonDecode(translated); + } catch (_) { + // 若模型返回包裹对象,尝试取字段 + try { + final obj = jsonDecode(translated); + arr = (obj is Map && obj['data'] is List) ? (obj['data'] as List) : []; + } catch (e) { + Logger.error('翻译结果 JSON 解析失败:${e.toString()}'); + return; + } + } + if (arr.isEmpty) return; + // 批量更新 + for (var item in arr) { + if (item is Map && item['id'] != null) { + await SqfliteApi.editMeetingSpeaker([ + { + 'id': item['id'], + 'content': (item['content'] ?? '').toString(), + } + ]); + } + } + // 刷新缓存 + if (id == meetingId) { + textList = await SqfliteApi.getMeetingSpeaker(id); + // 主动触发监听刷新 UI(MeetingDetailsController 通过监听 tasktype 同步 textList) + try { + tasktype.refresh(); + } catch (_) {} + } + } +} diff --git a/lib/data/services/meeting/meeting_upload_service.dart b/lib/data/services/meeting/meeting_upload_service.dart index 13a98ef2d..4472cc409 100644 --- a/lib/data/services/meeting/meeting_upload_service.dart +++ b/lib/data/services/meeting/meeting_upload_service.dart @@ -1,11 +1,15 @@ +import 'dart:io'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import '../../../core/utils/logger.dart'; import '../../../core/utils/upload_oss.dart'; +import '../usage_stats_service.dart'; import '../db/sqflite_api.dart'; import 'meeting_task_service.dart'; +import 'meeting_task_service2.dart'; +/// 管理会议音频文件上传服务 class MeetingUploadService extends GetxService { final _taskService = Get.find(); @@ -16,7 +20,7 @@ class MeetingUploadService extends GetxService { RxList uploadList = [].obs; bool _isUpload = false; - int _handled = 0; + int _handled = 0; //队列处理指针,表示当前循环语句处理的任务数量 @override void onInit() { @@ -24,6 +28,13 @@ class MeetingUploadService extends GetxService { _getUploadList(); } + void cancelUpload() { + _isUpload = false; + _handled = 0; + uploadList.clear(); + _storage.remove("meeting_upload_list"); + } + void _getUploadList() { uploadList.value = _storage.read("meeting_upload_list") ?? []; executeUpload(); @@ -36,6 +47,15 @@ class MeetingUploadService extends GetxService { String rootDir, { bool isUpload = true, }) async { + bool flash = false; + int fileSize = 0; + try { + fileSize = await File(filePath).length(); //获取文件大小 + final details = await SqfliteApi.getMeetingDetails(id); + final int seconds = (details['seconds'] ?? 0) as int; + flash = seconds > 0 && seconds <= 7200; + } catch (_) {} + _storage.write("meeting_service_type_$id", flash ? 'fast' : 'standard'); uploadList.add({ 'id': id, 'filepath': filePath, @@ -46,6 +66,8 @@ class MeetingUploadService extends GetxService { 'language': '', 'prompt': '', 'outline': '', + 'useFlashASR': flash, + 'fileSizeBytes': fileSize, }); _storage.write("meeting_upload_list", uploadList); if (isUpload) { @@ -63,27 +85,65 @@ class MeetingUploadService extends GetxService { if (element['audiourl'].isEmpty) { meetingId.value = element['id']; try { + int _sent = 0; + int _total = 0; final audiourl = await UploadOss.upload( filepath: element['filepath'], rootDir: element['rootdir'], onSendProgress: (int count, int total) { - uploadProgress.value = count / total; + // uploadProgress.value = count / total; + _sent = count; + _total = total; + uploadProgress.value = total > 0 ? count / total : 0.0; }, ); await SqfliteApi.editMeetingTitle( element['id'], {'audiourl': audiourl}, ); - if (element['isTask']) { - await _taskService.addTaskList( - element['id'], - audiourl, - '', - element['isSpeaker'], - element['language'], - element['prompt'], - element['outline'], + try { + // 记录对象存储用量 + final svc = Get.isRegistered() + ? Get.find() + : Get.put(UsageStatsService(), permanent: true); + int fileSizeBytes = 0; + try { + fileSizeBytes = element['fileSizeBytes'] ?? 0; + } catch (_) {} + final transferred = _total > 0 ? _total : fileSizeBytes; + svc.recordMeetingObjectStorage( + meetingId: element['id'], + storageUrl: audiourl, + storageBytes: fileSizeBytes, + transferBytes: transferred, + uploadTimestamp: DateTime.now().millisecondsSinceEpoch, ); + } catch (e) { + Logger.w('MeetingOSrecord', '对象存储用量记录失败: ' + e.toString()); + } + if (element['isTask']) { + if (element['useFlashASR'] == true) { + final service2 = Get.find(); + await service2.addTaskList( + element['id'], + audiourl, + isSpeaker: element['isSpeaker'], + language: element['language'], + targetLanguage: '', + prompt: element['prompt'], + outline: element['outline'], + ); + } else { + await _taskService.addTaskList( + element['id'], + audiourl, + '', + element['isSpeaker'], + element['language'], + element['prompt'], + element['outline'], + ); + } } element['audiourl'] = audiourl; uploadProgress.value = 0.0; diff --git a/lib/data/services/meeting/summary_task_service.dart b/lib/data/services/meeting/summary_task_service.dart new file mode 100644 index 000000000..c29741359 --- /dev/null +++ b/lib/data/services/meeting/summary_task_service.dart @@ -0,0 +1,187 @@ +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +import '../../../core/utils/logger.dart'; +import '../db/sqflite_api.dart'; +import '../deapsound_ai_service.dart'; +import 'meeting_task_service.dart'; +import 'meeting_task_service2.dart'; + +class SummaryTaskService extends GetxService { + final _taskService = Get.find(); + final _taskService2 = Get.find(); + + final GetStorage _storage = GetStorage(); + + List _taskList = []; + + bool _isTask = false; + + String summary = ''; //AI总结内容 + String overview = ''; //AI概览内容 + + @override + void onInit() { + super.onInit(); + _getTaskList(); + } + + void cancelTask() { + _taskList.clear(); + _storage.remove("summary_task_list"); + } + + void _getTaskList() { + _taskList = _storage.read("summary_task_list") ?? []; + _executeTask(); + } + + Future addTaskList( + int id, String text, String prompt, String outline) async { + Logger.debug("添加总结任务列表"); + _taskList.add({ + 'id': id, + 'text': text, + 'prompt': prompt, + 'outline': outline, + }); + _storage.write("summary_task_list", _taskList); + _executeTask(); + } + + void _executeTask() async { + if (_taskList.isEmpty) return; + if (_isTask) return; + _isTask = true; + var task = _taskList.first; + _aiServiceMessage( + task['id'], + task['text'], + task['prompt'], + task['outline'], + ); + Logger.debug("执行总结任务列表"); + } + + void removeTask(int id) { + _taskList.removeWhere((element) => element['id'] == id); + _storage.write("summary_task_list", _taskList); + } + + void _aiServiceMessage(int id, String text, String prompt, String outline) { + final ai = DeapsoundAIService(); + String summaryText = ''; + String overviewText = ''; + var summaryDone = false; + var overviewDone = false; + var summarySuccess = false; + var overviewSuccess = false; + + void tryFinish() { + if (summaryDone && overviewDone) { + if (summarySuccess && + overviewSuccess && + summaryText.isNotEmpty && + overviewText.isNotEmpty) { + _aiServiceValue(id, summaryText, overviewText); + } else { + _isTask = false; + _executeTask(); + } + } + } + + final summaryStream = ai.sendMessageStream(messages: [ + {'role': 'system', 'content': prompt}, + {'role': 'user', 'content': text}, + ]).timeout(const Duration(seconds: 120)); + + final overviewStream = ai.sendMessageStream(messages: [ + {'role': 'system', 'content': outline}, + {'role': 'user', 'content': text}, + ]).timeout(const Duration(seconds: 120)); + + summaryStream.listen((chunk) { + summaryText += chunk; + }, onError: (error) { + summaryDone = true; + summarySuccess = false; + tryFinish(); + }, onDone: () { + summaryDone = true; + summarySuccess = true; + tryFinish(); + }, cancelOnError: true); + + overviewStream.listen((chunk) { + overviewText += chunk; + }, onError: (error) { + overviewDone = true; + overviewSuccess = false; + tryFinish(); + }, onDone: () { + overviewDone = true; + overviewSuccess = true; + tryFinish(); + }, cancelOnError: true); + } + + Future _aiServiceValue( + int id, String summaryText, String overviewText) async { + final exists = _taskList.any((item) => item['id'] == id); + if (!exists) { + _isTask = false; + _executeTask(); + return; + } + await SqfliteApi.editMeeting(id, { + 'tasktype': 3, + 'summary': summaryText, + 'overview': overviewText, + }); + + // 更新 V1 Service 状态 + if (id == _taskService.meetingId) { + summary = summaryText; + overview = overviewText; + _taskService.tasktype.value = 3; + } + + // 更新 V2 Service 状态 + if (id == _taskService2.meetingId) { + summary = summaryText; + overview = overviewText; + _taskService2.summary = summaryText; + _taskService2.overview = overviewText; + _taskService2.tasktype.value = 3; + } + + // 更新 V1 任务完成列表 + Map taskFinishData = _taskService.taskFinishList.firstWhere( + (element) => element['id'] == id, + orElse: () => {}, + ); + if (taskFinishData.isNotEmpty) { + taskFinishData['tasktype'] = 3; + _taskService.taskFinishList.refresh(); + } + _storage.write("meeting_task_finish_list", _taskService.taskFinishList); + + // 更新 V2 任务完成列表 + Map taskFinishData2 = _taskService2.taskFinishList.firstWhere( + (element) => element['id'] == id, + orElse: () => {}, + ); + if (taskFinishData2.isNotEmpty) { + taskFinishData2['tasktype'] = 3; + _taskService2.taskFinishList.refresh(); + } + _storage.write("meeting_task_finish_list_v2", _taskService2.taskFinishList); + + _taskList.removeWhere((element) => element['id'] == id); + _storage.write("summary_task_list", _taskList); + _isTask = false; + _executeTask(); + Logger.debug("总结任务完成"); + } +} diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index f9831bdf9..c83f11906 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -136,6 +136,15 @@ class Api { ); } + //获取所有渠道包信息 + static getChannelApps() { + return DioManager().request( + NWMethod.post, + '/api/home/user_getchannelapps', + //params: params, + ); + } + //绑定设备 static binddevice([params]) { return DioManager().request( @@ -181,6 +190,15 @@ class Api { ); } + //询问订单 + static payverifyorder([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/pay_verifyorder', + params: params, + ); + } + //获取模版数据 static getEchomeetTemplates([params]) { return DioManager().request( @@ -217,11 +235,51 @@ class Api { ); } - //获取唤醒词 - static getwakeupvoices([params]) { + //获取商品列表 + static getGoods([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/pay_getgoodss', + params: params, + ); + } + + /// 资源使用/用量上报接口(`/api/home/user_usages`) + /// + /// 用途:上报翻译(Translation)和会议(Meet)的用量,服务端据此统计并回传最新余额/积分。 + /// + /// 当前在 Dart 侧的使用场景: + /// - 会议助手 ASR:`UsageStatsService._reportMeetingUsageToServer` + /// - 翻译/同传 ASR:`UsageStatsService._reportAsrUsageToServer` + /// + /// 参数(`params`)结构: + /// - `usagetype`:`int` 类型,使用场景类型(对应 `UsageType.toInt`),目前仅使用: + /// - `1`:翻译(Translation) + /// - `2`:会议(Meet) + /// - `usages`:`Map`,各项指标的键值对,例如: + /// - `MEETING_ASR_AUDIO_SECONDS`:会议识别音频时长(秒) + /// - `TRANSLATION_MODE_{mode}`:某翻译模式下累计有效识别秒数 + /// - `TRANSLATION_ASR_LANGUAGE_{language}`:某语言/语言对下的调用次数 + /// + /// 返回: + /// - 经过 `AuthInterceptor` 处理后,本方法返回值即为服务端响应体中的 `data` 字段。 + /// - 若上报成功,`data['usages']` 为 `Map`,内部各键的含义与积分/余额更新规则由服务端约定。 + /// + /// 使用示例(Dart): + /// ```dart + /// final params = { + /// 'usagetype': UsageType.Meet.toInt, + /// 'usages': { + /// 'MEETING_ASR_AUDIO_SECONDS': 120, + /// } + /// }; + /// final data = await Api.usagesResPoints(params); + /// final usages = (data['usages'] as Map?)?.cast(); + /// ``` + static usagesResPoints([params]) { return DioManager().request( NWMethod.post, - '/api/home/user_getwakeupvoices', + '/api/home/user_usages', params: params, ); } diff --git a/lib/data/services/network/auth_interceptor.dart b/lib/data/services/network/auth_interceptor.dart index 2933d5d62..c25accc3b 100644 --- a/lib/data/services/network/auth_interceptor.dart +++ b/lib/data/services/network/auth_interceptor.dart @@ -1,20 +1,33 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart' hide Response; +import 'dart:convert'; +import 'package:crypto/crypto.dart'; import 'package:get_storage/get_storage.dart'; import 'entity/base_entity.dart'; class AuthInterceptor extends Interceptor { final GetStorage _storage = GetStorage(); + final String _signKey = '@214%67g15q4*67m17#4l67!'; // 替换为实际的密钥 @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { var jsonString = _storage.read("logintoken"); options.headers['Authorization'] = jsonString; + final timestamp = DateTime.now().millisecondsSinceEpoch.toString(); + options.headers['Ts'] = timestamp; + options.headers['Sign'] = _generateSign(timestamp); handler.next(options); } + String _generateSign(String timestamp) { + // 按照“MD5(私钥 + 时间戳)”生成签名,与 Go 端保持一致 + final signStr = 'ts=${timestamp}key=${_signKey}'; + print("signStr: $signStr"); + return md5.convert(utf8.encode(signStr)).toString(); + } + @override void onResponse(Response response, ResponseInterceptorHandler handler) async { BaseEntity entity = BaseEntity.fromJson(response.data); diff --git a/lib/data/services/network/dio_manager.dart b/lib/data/services/network/dio_manager.dart index 0496c11f9..43de8d723 100644 --- a/lib/data/services/network/dio_manager.dart +++ b/lib/data/services/network/dio_manager.dart @@ -14,9 +14,9 @@ class DioManager { String get baseUrl { final countryCode = Get.deviceLocale?.countryCode?.toUpperCase(); final isChinaMainland = countryCode == 'CN'; - - if (!isChinaMainland) { - return dotenv.env['HW_SERVER_URL'] ?? dotenv.env['SERVER_URL'] ?? ''; + final channel = dotenv.env['Channel'] ?? 'OFFICIAL'; + if (!isChinaMainland || channel == "GLOBAL_GOOGLE") { + return dotenv.env['HW_SERVER_URL'] ?? ''; } else { return dotenv.env['SERVER_URL'] ?? ''; } @@ -34,6 +34,16 @@ class DioManager { dio = Dio(options); dio.interceptors.add(AuthInterceptor()); + // 调试:输出请求与响应的详细日志,便于排查服务端创建订单或查询订单问题 + dio.interceptors.add(LogInterceptor( + request: true, + requestHeader: true, + requestBody: true, + responseHeader: false, + responseBody: true, + error: true, + logPrint: (obj) => print('[DIO] $obj'), + )); } // 请求,返回参数为 T diff --git a/lib/data/services/novice_guide_service.dart b/lib/data/services/novice_guide_service.dart index f7617114f..10e9195a2 100644 --- a/lib/data/services/novice_guide_service.dart +++ b/lib/data/services/novice_guide_service.dart @@ -23,10 +23,10 @@ class NoviceGuideService extends GetxService { List aiStepsList = [ { - 'start': '对小言说:“今天天气怎么样”', - 'input': '今天天气怎么样', - 'send': '小言会为你实时为你播报天气', - 'end': '小言能实时为你播报天气,试试其他指令吧', + 'startKey': 'guideWeatherStart', + 'inputKey': 'guideWeatherInput', + 'sendKey': 'guideWeatherSend', + 'endKey': 'guideWeatherEnd', 'method': () async { // 天气查询需要位置权限 await PermissionUtil.instance.requestPermission( @@ -39,17 +39,17 @@ class NoviceGuideService extends GetxService { }, }, { - 'start': '对小言说:“请播放一组摇滚乐”', - 'input': '请播放一组摇滚乐', - 'send': '小言会为你搜寻到一组音乐', - 'end': '小言已成功为您搜寻到一组音乐,试试其他指令吧', + 'startKey': 'guideMusicStart', + 'inputKey': 'guideMusicInput', + 'sendKey': 'guideMusicSend', + 'endKey': 'guideMusicEnd', 'method': () async {}, }, { - 'start': '对小言说:“导航到深圳机场”', - 'input': '导航到深圳机场', - 'send': '小言会为你规划路线', - 'end': '小言已成功为您导航到深圳机场,试试其他指令吧', + 'startKey': 'guideNavStart', + 'inputKey': 'guideNavInput', + 'sendKey': 'guideNavSend', + 'endKey': 'guideNavEnd', 'method': () async { // 导航功能需要位置权限 await PermissionUtil.instance.requestPermission( @@ -62,10 +62,10 @@ class NoviceGuideService extends GetxService { }, }, { - 'start': '对小言说:“明天下午3点创建会议提醒”', - 'input': '明天下午3点创建会议提醒', - 'send': '小言会为你创建会议提醒', - 'end': '小言已成功为您创建会议提醒,试试其他指令吧', + 'startKey': 'guideMeetingStart', + 'inputKey': 'guideMeetingInput', + 'sendKey': 'guideMeetingSend', + 'endKey': 'guideMeetingEnd', 'method': () async { //创建会议提醒需要日历权限 await PermissionUtil.instance.requestPermission( @@ -78,10 +78,10 @@ class NoviceGuideService extends GetxService { }, }, { - 'start': '对小言说:“拨打妈妈的电话”', - 'input': '拨打妈妈的电话', - 'send': '小言会为你拨打电话', - 'end': '小言已成功为您拨打电话,试试其他指令吧', + 'startKey': 'guidePhoneStart', + 'inputKey': 'guidePhoneInput', + 'sendKey': 'guidePhoneSend', + 'endKey': 'guidePhoneEnd', 'method': () async { if (Platform.isAndroid) { // 拨打电话需要电话权限 @@ -103,19 +103,12 @@ class NoviceGuideService extends GetxService { }, }, { - 'start': '对小言说:“给爸爸发短信说晚上回家吃饭’”', - 'input': '给爸爸发短信说晚上回家吃饭’', - 'send': '小言会为你发送短信', - 'end': '短信已发送,语音操控更便捷', + 'startKey': 'guideSmsStart', + 'inputKey': 'guideSmsInput', + 'sendKey': 'guideSmsSend', + 'endKey': 'guideSmsEnd', 'method': () async { if (Platform.isAndroid) { - // 发送短信需要短信权限 - await PermissionUtil.instance.requestPermission( - permissionType: Permission.sms, - permissionName: 'sms'.tr, // 短信 - explanationText: 'smsPermissionDesc'.tr, // 用于发送短信功能 - permanentDenialText: 'smsPermissionDetailDesc'.tr, // 发送短信功能需要短信权限 - ); //发送短信还需要联系人权限 await PermissionUtil.instance.requestPermission( permissionType: Permission.contacts, diff --git a/lib/data/services/ota_check_service.dart b/lib/data/services/ota_check_service.dart index 5caa31493..13a5f09d5 100644 --- a/lib/data/services/ota_check_service.dart +++ b/lib/data/services/ota_check_service.dart @@ -57,6 +57,7 @@ class OtaCheckService extends GetxService { /// [isFullyConnected] 设备是否完全连接 /// 返回是否成功执行了检查 Future performOtaCheck(bool isFullyConnected) async { + bool isForceUpdate = false; try { // 检查是否应该进行OTA检查 if (!shouldCheckOta()) { @@ -95,7 +96,12 @@ class OtaCheckService extends GetxService { _otaCoreService.isNewVersionAvailable(currentVersion, latestVersion); if (hasUpdate) { - Logger.d(_tag, '发现新版本,准备显示升级提醒'); + isForceUpdate = latestInfo['isForceUpdate'] as bool? ?? false; + if (isForceUpdate) { + Logger.d(_tag, '发现强制更新版本,准备显示升级提醒'); + } else { + Logger.d(_tag, '发现新版本,准备显示升级提醒'); + } // 再次确认当前在Home页面(防止异步执行期间页面切换) if (_isCurrentlyOnHomePage()) { @@ -109,12 +115,18 @@ class OtaCheckService extends GetxService { } // 标记为已检查(无论是否有更新都标记为已检查) - _markAsChecked(); + if (!isForceUpdate) { + _markAsChecked(); + } + _isCheckingInProgress = false; return true; } catch (e) { Logger.e(_tag, '检查OTA升级失败: $e'); // 即使检查失败,也标记为已检查,避免重复尝试 - _markAsChecked(); + if (!isForceUpdate) { + _markAsChecked(); + } + _isCheckingInProgress = false; return false; } } diff --git a/lib/data/services/pay_service.dart b/lib/data/services/pay_service.dart index 429b223ae..4417446a9 100644 --- a/lib/data/services/pay_service.dart +++ b/lib/data/services/pay_service.dart @@ -1,6 +1,17 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:agent_service/agent_service.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import '/data/services/network/api.dart'; import 'package:tobias/tobias.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:in_app_purchase_android/in_app_purchase_android.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import '../../data/models/user_Info.dart'; import '../../modules/login/controllers/weixinLogin.dart'; @@ -10,6 +21,17 @@ typedef PaymentResultCallback = void Function(bool success, String message); class PayService extends GetxService { late FlWX _flwx; late Tobias _tobias; // 添加 Tobias 实例 + String? _lastOrderId; // 最近一次创建的订单号 + // 内购 + final InAppPurchase _inAppPurchase = InAppPurchase.instance; + StreamSubscription>? _iapSubscription; + final RxBool iapAvailable = false.obs; + PaymentResultCallback? _iapOnResult; + // 最近一次 IAP 购买上下文,用于主动查询时携带验证参数 + PurchaseDetails? _lastIapPurchase; + ProductDetails? _lastIapProduct; + + String? get lastOrderId => _lastOrderId; @override void onInit() { @@ -28,12 +50,13 @@ class PayService extends GetxService { // 初始化支付宝SDK _tobias = Tobias(); // 初始化 Tobias 实例 _initAlipay(); + _initIAP(); } /// 初始化支付宝SDK Future _initAlipay() async { try { - // 检查支付宝是否安装 - 修复用法 + // 检查支付宝是否安x装 - 修复用法 final isInstalled = await _tobias.isAliPayInstalled; print('支付宝是否安装: $isInstalled'); } catch (e) { @@ -41,6 +64,443 @@ class PayService extends GetxService { } } + /// 初始化内购(Apple/Google) + Future _initIAP() async { + try { + // Web 环境不支持内购 + if (kIsWeb) { + iapAvailable.value = false; + return; + } + final available = await _inAppPurchase.isAvailable(); + iapAvailable.value = available; + if (!available) return; + + // 监听购买结果流 + _iapSubscription?.cancel(); + _iapSubscription = _inAppPurchase.purchaseStream.listen( + _handlePurchaseUpdates, + onDone: () => _iapSubscription?.cancel(), + onError: (e) { + _iapOnResult?.call(false, '内购异常: $e'); + }, + ); + } catch (e) { + print('初始化内购失败: $e'); + } + } + + /// 调试:批量查询并打印内购商品详情(用于确认商品ID是否存在) + Future debugQueryProducts(Set productIds) async { + try { + // 先原样打印传入的 productIds,便于核对入参 + print('[IAP] debugQueryProducts 输入 IDs(Set): $productIds'); + final idsList = productIds.toList(); + print('[IAP] debugQueryProducts 输入 IDs(List): ${idsList.join(', ')}'); + for (final id in productIds) { + print('[IAP] 待查询商品ID: $id'); + } + // 仅在非 Web 平台执行;平台信息用于日志 + final platform = _platformString(); + if (!iapAvailable.value) { + final available = await _inAppPurchase.isAvailable(); + iapAvailable.value = available; + } + if (!iapAvailable.value) { + print('[IAP-$platform] 当前平台内购不可用,无法查询商品。'); + return; + } + print('[IAP-$platform] 开始查询商品,IDs=${productIds.join(', ')}'); + final resp = await _inAppPurchase.queryProductDetails(productIds); + if (resp.error != null) { + final err = resp.error!; + print( + '[IAP-$platform] 查询错误: code=${err.code}, source=${err.source}, message=${err.message}, details=${err.details}'); + } + if (resp.productDetails.isEmpty) { + print('[IAP-$platform] 未返回任何商品。notFound=${resp.notFoundIDs}'); + } else { + for (final p in resp.productDetails) { + print( + '[IAP-$platform] 商品: id=${p.id}, title=${p.title}, price=${p.price}, currency=${p.currencyCode}'); + } + if (resp.notFoundIDs.isNotEmpty) { + print('[IAP-$platform] 未找到的IDs: ${resp.notFoundIDs}'); + } + } + } catch (e) { + print('[IAP] 调试查询异常: $e'); + } + } + + Future _queryIapProduct(String productId) async { + try { + final pid = productId.trim(); + print('[IAP] 开始查询单个商品: id="$pid"'); + final resp = await _inAppPurchase.queryProductDetails({pid}); + if (resp.error != null) { + final err = resp.error!; + print( + '[IAP] 查询商品失败: code=${err.code}, source=${err.source}, message=${err.message}, details=${err.details}'); + } + if (resp.productDetails.isEmpty) { + print('[IAP] 查询结果为空。notFound=${resp.notFoundIDs}'); + return null; + } + for (final p in resp.productDetails) { + print( + '[IAP] 命中商品: id=${p.id}, title=${p.title}, price=${p.price}, currency=${p.currencyCode}'); + } + // 避免 Dart 泛型推断在 Android 平台出现 orElse 类型不匹配(ProductDetails vs GooglePlayProductDetails) + ProductDetails? matched; + for (final p in resp.productDetails) { + if (p.id == pid) { + matched = p; + break; + } + } + return matched ?? resp.productDetails.first; + } catch (e) { + print('[IAP] 查询内购商品异常: $e'); + return null; + } + } + + /// Apple/Google 内购统一流程(消耗型) + Future _processIapPayment({ + required String productId, + PaymentResultCallback? onResult, + }) async { + _iapOnResult = onResult; + print( + '[IAP] 准备发起购买: id="${productId.trim()}" iapAvailable=${iapAvailable.value}'); + if (!iapAvailable.value) { + onResult?.call(false, '内购不可用'); + return false; + } + final product = await _queryIapProduct(productId); + if (product == null) { + onResult?.call(false, '未找到内购商品: $productId'); + return false; + } + try { + // 记录最近一次用于购买的商品详情,便于后续构建查询参数 + _lastIapProduct = product; + final isAndroid = + !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + bool ok; + if (isAndroid && product is GooglePlayProductDetails) { + // 通过环境变量决定订阅或一次性商品路径,避免访问不兼容的属性 + final isSubscription = + _getEnvInt('APP_IAP_GOOGLE_IS_SUBSCRIPTION', defaultValue: 0) == 1; + if (isSubscription) { + print('[IAP-android] 订阅商品(ENV),使用 buyNonConsumable'); + final gpParam = GooglePlayPurchaseParam(productDetails: product); + print( + '[IAP-android] 调用 buyNonConsumable 参数: productId=${product.id}'); + ok = await _inAppPurchase.buyNonConsumable(purchaseParam: gpParam); + print('[IAP-android] buyNonConsumable 返回: $ok'); + } else { + // 普通一次性/消耗型商品,使用 buyConsumable + final purchaseParam = PurchaseParam(productDetails: product); + print( + '[IAP-android] 调用 buyConsumable 参数: productId=${product.id} autoConsume=true'); + ok = await _inAppPurchase.buyConsumable( + purchaseParam: purchaseParam, + autoConsume: true, + ); + print('[IAP-android] buyConsumable 返回: $ok'); + } + } else { + // iOS 或非订阅商品,按消耗型处理(如需支持 iOS 订阅,可通过 ENV 切换调用 buyNonConsumable) + final purchaseParam = PurchaseParam(productDetails: product); + print( + '[IAP-ios] 调用 buyConsumable 参数: productId=${product.id} autoConsume=true'); + ok = await _inAppPurchase.buyConsumable( + purchaseParam: purchaseParam, + autoConsume: true, + ); + print('[IAP-ios] buyConsumable 返回: $ok'); + } + if (!ok) { + onResult?.call(false, '无法发起内购'); + print('[IAP] 发起内购失败:平台=${_platformString()} productId=${product.id}'); + } else { + onResult?.call(true, '已发起内购,请完成支付'); + print('[IAP] 已发起内购:等待购买流回调(可能受旧交易阻塞或商店限制)'); + } + return ok; + } catch (e) { + if (e is PlatformException && + (e.code.contains('purchase_cancelled') || + e.code.contains('storekit2_purchase_cancelled'))) { + onResult?.call(false, '已取消支付'); + print( + '[IAP] 用户取消支付(同步抛出) code=${e.code} message=${e.message} product=${product.id}'); + return false; + } + onResult?.call(false, '发起内购失败: $e'); + print('[IAP] 发起内购异常: $e'); + return false; + } + } + + Future _handlePurchaseUpdates(List detailsList) async { + for (final details in detailsList) { + // 记录最近一次 IAP 购买详情 + _lastIapPurchase = details; + print( + '[IAP] 更新: status=${details.status} product=${details.productID} purchaseID=${details.purchaseID} error=${details.error}'); + switch (details.status) { + case PurchaseStatus.pending: + _iapOnResult?.call(true, '支付处理中…'); + break; + case PurchaseStatus.error: + print( + '[IAP] 错误: code=${details.error?.code} message=${details.error?.message}'); + _iapOnResult?.call(false, details.error?.message ?? '支付错误'); + break; + case PurchaseStatus.purchased: + final txId = details.purchaseID ?? ''; + final ok = await _verifyIapWithServer(details); + if (!ok) { + print( + '[IAP] 验证失败 purchased txId=${details.purchaseID} product=${details.productID}'); + _iapOnResult?.call(false, details.error?.message ?? '支付错误'); + } + if (ok) { + try { + await _inAppPurchase.completePurchase(details); + print('[IAP] 完成交易 purchased txId=${details.purchaseID}'); + _iapOnResult?.call(true, details.error?.message ?? '支付成功'); + } catch (e, st) { + print('[IAP] 完成交易异常 purchased txId=${details.purchaseID} err=$e'); + print('[IAP] 完成交易异常堆栈 purchased $st'); + } + } + + break; + case PurchaseStatus.restored: + final txId = details.purchaseID ?? ''; + final ok = await _verifyIapWithServer(details); + if (!ok) { + print( + '[IAP] 验证失败 restored(silent) txId=${details.purchaseID} product=${details.productID}'); + _iapOnResult?.call(false, details.error?.message ?? '支付错误'); + } + if (ok) { + try { + await _inAppPurchase.completePurchase(details); + _iapOnResult?.call(true, details.error?.message ?? '支付成功'); + print('[IAP] 完成交易 restored(silent) txId=${details.purchaseID}'); + } catch (e, st) { + print( + '[IAP] 完成交易异常 restored(silent) txId=${details.purchaseID} err=$e'); + print('[IAP] 完成交易异常堆栈 restored(silent) $st'); + } + } + break; + case PurchaseStatus.canceled: + _iapOnResult?.call(false, '已取消支付'); + print('[IAP] 用户取消支付 product=${details.productID}'); + break; + } + } + } + + Future _verifyIapWithServer(PurchaseDetails details) async { + try { + final params = await buildIapAskParams(); + final resp = await Api.payverifyorder(params); + Map? user; + Map? order; + if (resp is Map) { + if (resp['user'] is Map) { + user = resp['user'] as Map; + } else if (resp['data'] is Map && + (resp['data']['user'] is Map)) { + user = resp['data']['user'] as Map; + } + if (resp['order'] is Map) { + order = resp['order'] as Map; + } else if (resp['data'] is Map && + (resp['data']['order'] is Map)) { + order = resp['data']['order'] as Map; + } + } + final int? status = order != null + ? (order['status'] is int + ? order['status'] as int + : int.tryParse('${order['status']}')) + : null; + final bool ok = status == 1 || user != null; + if (ok && user != null) { + User.instance.updateUserInfo(user); + setVipExptime(User.instance.vipexptime); + } + if (!ok) { + final code = (resp is Map) ? resp['code'] : null; + final msg = (resp is Map) ? (resp['message'] ?? resp['msg']) : null; + print( + '[IAP] 验证失败详情 status=$status code=$code msg=$msg order_id=${params['order_id']}'); + } + return ok; + } catch (e) { + print('[IAP] 服务器验证异常: $e'); + return false; + } + } + + /// 构建 Apple/Google 主动查询(pay_askorder)所需的验证参数 + /// 返回与后端 PayAskOrCancelReq 对应的 Map(不再携带 apple_sandbox) + Future> buildIapAskParams() async { + final params = {}; + if (_lastOrderId != null && _lastOrderId!.isNotEmpty) { + params['order_id'] = _lastOrderId; + } + + final platform = _platformString(); + if (platform == 'ios') { + final receiptOrData = + _lastIapPurchase?.verificationData.serverVerificationData ?? ''; + final txId = _lastIapPurchase?.purchaseID ?? ''; + // 是否按交易ID校验,默认 false,可通过环境变量开启 + final useTxIdEnv = _getEnvBool('APP_IAP_USE_TXID', defaultValue: false); + params['apple_receipt_data'] = useTxIdEnv ? txId : receiptOrData; + params['apple_use_transaction_id'] = useTxIdEnv; + } else if (platform == 'android') { + final packageInfo = await PackageInfo.fromPlatform(); + final pkg = packageInfo.packageName; + final token = + _lastIapPurchase?.verificationData.serverVerificationData ?? ''; + final productId = + _lastIapPurchase?.productID ?? _lastIapProduct?.id ?? ''; + // 订阅标识,默认 false;如需严格区分,可在商品元数据或环境变量中配置 + final isSub = + _getEnvBool('APP_IAP_GOOGLE_IS_SUBSCRIPTION', defaultValue: false); + params['google_package_name'] = pkg; + params['google_product_id'] = productId; + params['google_purchase_token'] = token; + params['google_is_subscription'] = isSub; + } + return params; + } + + bool _getEnvBool(String key, {required bool defaultValue}) { + try { + final v = dotenv.env[key]; + if (v != null && v.isNotEmpty) { + final low = v.toLowerCase(); + return low == 'true' || low == '1' || low == 'yes'; + } + } catch (_) {} + return defaultValue; + } + + int _getEnvInt(String key, {required int defaultValue}) { + try { + final v = dotenv.env[key]; + if (v != null && v.isNotEmpty) { + return int.tryParse(v.trim()) ?? defaultValue; + } + } catch (_) {} + return defaultValue; + } + + String _platformString() { + if (kIsWeb) return 'web'; + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + return 'ios'; + case TargetPlatform.android: + return 'android'; + case TargetPlatform.macOS: + return 'macos'; + case TargetPlatform.windows: + return 'windows'; + case TargetPlatform.linux: + return 'linux'; + case TargetPlatform.fuchsia: + return 'fuchsia'; + } + } + + /// 解析 Apple 的真实商品ID(支持环境前缀/后缀或显式映射) + /// 优先级:显式映射(APP_IOS_IAP_ID_MAP JSON) > 前缀(APP_IOS_IAP_PREFIX) > 后缀(APP_IOS_IAP_SUFFIX) > 原始ID + /// 示例: + /// - APP_IOS_IAP_PREFIX=com.voitrans. + /// - APP_IOS_IAP_ID_MAP={"ai_001":"com.voitrans.ai001","ai_002":"com.voitrans.ai002"} + String resolveAppleProductId(String backendId) { + final raw = backendId.trim(); + Map? mapped; + try { + final jsonStr = dotenv.env['APP_IOS_IAP_ID_MAP']; + if (jsonStr != null && jsonStr.isNotEmpty) { + mapped = json.decode(jsonStr) as Map; + } + } catch (e) { + print('[IAP-ios] 解析 APP_IOS_IAP_ID_MAP 失败: $e'); + } + if (mapped != null) { + final m = mapped[raw]; + if (m is String && m.trim().isNotEmpty) { + final resolved = m.trim(); + print('[IAP-ios] 使用映射ID: $raw -> $resolved'); + return resolved; + } + } + final prefix = dotenv.env['APP_IOS_IAP_PREFIX'] ?? ''; + final suffix = dotenv.env['APP_IOS_IAP_SUFFIX'] ?? ''; + final resolved = '${prefix}${raw}${suffix}'; + if (resolved != raw) { + print('[IAP-ios] 使用前后缀ID: $raw -> $resolved'); + } else { + print('[IAP-ios] 未配置映射/前缀,使用原始ID: $raw'); + } + return resolved; + } + + /// 发起 Apple 内购 + Future processApplePayment({ + required String productId, + PaymentResultCallback? onResult, + }) async { + if (kIsWeb || defaultTargetPlatform != TargetPlatform.iOS) { + onResult?.call(false, '当前平台不支持 Apple 内购'); + return false; + } + if (!iapAvailable.value) { + await _initIAP(); + if (!iapAvailable.value) { + onResult?.call(false, '内购不可用'); + return false; + } + } + // 解析为 App Store 的真实商品ID + final iosId = resolveAppleProductId(productId); + print('[IAP-ios] 准备使用商品ID购买: backend="$productId" -> store="$iosId"'); + return _processIapPayment(productId: iosId, onResult: onResult); + } + + /// 发起 Google Play 内购 + Future processGooglePayment({ + required String productId, + PaymentResultCallback? onResult, + }) async { + if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) { + onResult?.call(false, '当前平台不支持 Google 内购'); + return false; + } + if (!iapAvailable.value) { + await _initIAP(); + if (!iapAvailable.value) { + onResult?.call(false, '内购不可用'); + return false; + } + } + return _processIapPayment(productId: productId, onResult: onResult); + } + /// 查询支付结果 /// /// [orderId] 订单ID @@ -142,7 +602,7 @@ class PayService extends GetxService { 'product_name': productName, 'product_desc': productDesc ?? '', 'amount': amount, - 'payment_method': paymentMethod, + 'payment_method': 4, 'client_ip': clientIp ?? '', 'notify_url': notifyUrl ?? '', 'extra_data': extraData ?? '', @@ -150,9 +610,36 @@ class PayService extends GetxService { print('创建支付订单: $response'); // 检查响应是否成功 if (response != null) { + // 尝试提取订单号,兼容多种返回字段 + String? orderId; + final candidates = [ + 'order_id', + 'orderId', + 'out_trade_no', + 'outTradeNo', + ]; + for (final key in candidates) { + final v = response[key]; + if (v is String && v.isNotEmpty) { + orderId = v; + break; + } + } + if (orderId == null && response['data'] is Map) { + final data = response['data'] as Map; + for (final key in candidates) { + final v = data[key]; + if (v is String && v.isNotEmpty) { + orderId = v; + break; + } + } + } + _lastOrderId = orderId; + final pay_params = response["pay_params"] as Map; // 返回微信支付所需的参数 - return { + final result = { 'appId': pay_params['appid'], 'partnerId': pay_params['partnerid'], 'prepayId': pay_params['prepayid'], @@ -161,6 +648,10 @@ class PayService extends GetxService { 'timestamp': pay_params['timestamp'], 'sign': pay_params['sign'], }; + if (orderId != null) { + result['orderId'] = orderId; + } + return result; } else { print('创建支付订单失败: ${response?['message'] ?? '未知错误'}'); return null; @@ -231,7 +722,6 @@ class PayService extends GetxService { productName: description, productDesc: description, amount: amount, - paymentMethod: 3, // 支付宝支付 notifyUrl: 'https://api.ideapsound.com/api/home/pay_alipaynotify', ); @@ -326,7 +816,6 @@ class PayService extends GetxService { required String productName, String? productDesc, required int amount, - int paymentMethod = 2, // 2表示支付宝 String? clientIp, String? notifyUrl, String? extraData, @@ -338,7 +827,7 @@ class PayService extends GetxService { 'product_name': productName, 'product_desc': productDesc ?? '', 'amount': amount, - 'payment_method': paymentMethod, + 'payment_method': 3, 'client_ip': clientIp ?? '', 'notify_url': notifyUrl ?? '', 'extra_data': extraData ?? '', @@ -348,6 +837,32 @@ class PayService extends GetxService { // 检查响应是否成功 if (response != null && response['alipay_pay_params'] != null) { + // 解析订单号并缓存 + String? orderId; + final candidates = [ + 'order_id', + 'orderId', + 'out_trade_no', + 'outTradeNo', + ]; + for (final key in candidates) { + final v = response[key]; + if (v is String && v.isNotEmpty) { + orderId = v; + break; + } + } + if (orderId == null && response['data'] is Map) { + final data = response['data'] as Map; + for (final key in candidates) { + final v = data[key]; + if (v is String && v.isNotEmpty) { + orderId = v; + break; + } + } + } + _lastOrderId = orderId; // 返回支付宝订单信息字符串 return response['alipay_pay_params'] as String?; } else { @@ -359,4 +874,298 @@ class PayService extends GetxService { return null; } } + + /// 创建 PayPal 支付订单 + /// 返回需要打开的支付授权链接(approval_url)或支付页面URL + Future createPaypalOrder({ + required String productId, + required String productName, + String? productDesc, + required int amount, + String? clientIp, + String? notifyUrl, + String? extraData, + }) async { + try { + // 回跳地址:自定义 Scheme,分别用于支付完成与取消 + const returnUrl = 'deepvoiceclient://paypal/return'; + const cancelUrl = 'deepvoiceclient://paypal/cancel'; + final response = await Api.paycreateorder({ + 'product_id': productId, + 'product_name': productName, + 'product_desc': productDesc ?? '', + 'amount': amount, + 'payment_method': 5, + 'client_ip': clientIp ?? '', + 'notify_url': notifyUrl ?? '', + 'extra_data': extraData ?? '', + // 供后端设置 PayPal application_context 用于网页授权回跳 + 'return_url': returnUrl, + 'cancel_url': cancelUrl, + }); + + if (response != null) { + print('创建 PayPal 订单响应: $response'); + // 1) 提取并缓存平台订单号(用于回查) + String? appOrderId; + const candidates = [ + 'order_id', + 'orderId', + 'out_trade_no', + 'outTradeNo', + ]; + for (final key in candidates) { + final v = response[key]; + if (v is String && v.isNotEmpty) { + appOrderId = v; + break; + } + } + final data = response['data']; + if (appOrderId == null && data is Map) { + for (final key in candidates) { + final v = data[key]; + if (v is String && v.isNotEmpty) { + appOrderId = v; + break; + } + } + } + _lastOrderId = appOrderId; // 记录APP侧订单号(如 APP2025...),用于 pay_askorder + + // 2) 优先解析 paypal_pay_params 字段(后端返回JSON字符串,含 links) + String? rawParams; + final topParams = response['paypal_pay_params']; + final dataParams = + (data is Map) ? data['paypal_pay_params'] : null; + if (topParams is String && topParams.isNotEmpty) { + rawParams = topParams; + } else if (dataParams is String && dataParams.isNotEmpty) { + rawParams = dataParams; + } + + if (rawParams != null) { + try { + final parsed = jsonDecode(rawParams); + if (parsed is Map) { + final links = parsed['links']; + if (links is List) { + for (final item in links) { + if (item is Map) { + final rel = item['rel']?.toString(); + final hrefRaw = item['href']?.toString(); + if (rel == 'approve' && + hrefRaw != null && + hrefRaw.isNotEmpty) { + // 清理日志中可能出现的反引号等噪音字符 + final href = hrefRaw.replaceAll('`', '').trim(); + print( + '解析到 PayPal 授权链接(links.approve): $href, appOrderId: $appOrderId'); + return href; + } + } + } + } + } + } catch (e) { + print('解析 paypal_pay_params 失败: $e'); + } + } + + // 3) 回退:尝试从常见字段直接取URL + final urlKeys = [ + 'paypal_pay_url', + 'approval_url', + 'paypalApprovalUrl', + 'pay_url', + 'payment_url', + 'url', + ]; + for (final key in urlKeys) { + final url = response[key]; + if (url is String && url.isNotEmpty) { + print('解析到 PayPal 授权链接($key): $url, appOrderId: $appOrderId'); + return url; + } + } + if (data is Map) { + for (final key in urlKeys) { + final url = data[key]; + if (url is String && url.isNotEmpty) { + print( + '解析到 PayPal 授权链接(data.$key): $url, appOrderId: $appOrderId'); + return url; + } + } + } + } + + print('创建 PayPal 订单失败或未返回有效URL: $response'); + return null; + } catch (e) { + print('创建 PayPal 订单异常: $e'); + return null; + } + } + + /// 拉起 PayPal 支付(打开授权链接)。 + /// 注意:该方法仅负责跳转至 PayPal 页面,结果需要后端回调通知。 + Future paypalPay({ + required String approvalUrl, + PaymentResultCallback? onResult, + }) async { + try { + print('准备打开 PayPal 授权页面: $approvalUrl'); + final uri = Uri.parse(approvalUrl); + final ok = await launchUrl(uri, mode: LaunchMode.externalApplication); + if (ok) { + onResult?.call(true, '已打开 PayPal,请完成支付'); + } else { + onResult?.call(false, '无法打开 PayPal 支付页面'); + } + return ok; + } catch (e) { + onResult?.call(false, '打开 PayPal 异常: $e'); + return false; + } + } + + /// 一体化的 PayPal 支付流程:创建订单 -> 打开授权链接 + Future processPaypalPayment({ + required String productId, + required int amount, + required String description, + PaymentResultCallback? onResult, + }) async { + try { + final approvalUrl = await createPaypalOrder( + productId: productId, + productName: description, + productDesc: description, + amount: amount, + notifyUrl: 'https://api.ideapsound.com/api/home/pay_paypalnotify', + ); + + if (approvalUrl == null) { + onResult?.call(false, '创建 PayPal 订单失败'); + return false; + } + + return await paypalPay(approvalUrl: approvalUrl, onResult: onResult); + } catch (e) { + onResult?.call(false, 'PayPal 支付流程异常: $e'); + return false; + } + } + + /// 创建 Apple 内购订单,仅用于在服务端生成并记录订单号,后续回查使用 + /// 返回服务端生成的 `order_id`,若失败则返回 null + Future createAppleIapOrder({ + required String productId, + required String productName, + String? productDesc, + required int amount, + }) async { + try { + final resp = await Api.paycreateorder({ + 'product_id': productId, + 'product_name': productName, + 'product_desc': productDesc ?? '', + 'amount': amount, + 'payment_method': 6, + }); + if (resp != null) { + String? orderId; + const candidates = [ + 'order_id', + 'orderId', + 'out_trade_no', + 'outTradeNo' + ]; + for (final k in candidates) { + final v = resp[k]; + if (v is String && v.isNotEmpty) { + orderId = v; + break; + } + } + if (orderId == null && resp['data'] is Map) { + final data = resp['data'] as Map; + for (final k in candidates) { + final v = data[k]; + if (v is String && v.isNotEmpty) { + orderId = v; + break; + } + } + } + _lastOrderId = orderId; + return orderId; + } + return null; + } catch (e) { + print('创建 Apple IAP 订单异常: $e'); + return null; + } + } + + /// 创建 Google 内购订单,仅用于在服务端生成并记录订单号,后续回查使用 + /// 返回服务端生成的 `order_id`,若失败则返回 null + Future createGoogleIapOrder({ + required String productId, + required String productName, + String? productDesc, + required int amount, + }) async { + try { + final resp = await Api.paycreateorder({ + 'product_id': productId, + 'product_name': productName, + 'product_desc': productDesc ?? '', + 'amount': amount, + 'payment_method': 7, + }); + if (resp != null) { + String? orderId; + const candidates = [ + 'order_id', + 'orderId', + 'out_trade_no', + 'outTradeNo' + ]; + for (final k in candidates) { + final v = resp[k]; + if (v is String && v.isNotEmpty) { + orderId = v; + break; + } + } + if (orderId == null && resp['data'] is Map) { + final data = resp['data'] as Map; + for (final k in candidates) { + final v = data[k]; + if (v is String && v.isNotEmpty) { + orderId = v; + break; + } + } + } + _lastOrderId = orderId; + return orderId; + } + return null; + } catch (e) { + print('创建 Google IAP 订单异常: $e'); + return null; + } + } + + /// 请求蓝牙权限 + void setVipExptime(int vipexptime) { + try { + AgentService.setVipExptime(vipexptime); + } catch (e) { + print('请求蓝牙权限失败: ${e.toString()}'); + } + } } diff --git a/lib/data/services/speech_impl/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart index 12e43489c..1eb84856b 100644 --- a/lib/data/services/speech_impl/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -191,7 +191,10 @@ class AzureAsrService extends GetxService implements AsrService { /// - 在 600ms 的冷却时间内再次调用会被防抖拦截并返回 false; /// - 首次或冷却期外调用会继续启动连续识别; /// - 若当前已处于连续识别中,会先调用停止后再启动。 - Future startContinuousRecognition(bool audioSourceType) async { + Future startContinuousRecognition( + bool audioSourceType, { + bool isRemoveFirstPunctuation = true, + }) async { // 防抖判断:短时间内重复调用直接拦截 final now = DateTime.now(); if (_lastStartAt != null && now.difference(_lastStartAt!) < _debounce) { @@ -212,6 +215,7 @@ class AzureAsrService extends GetxService implements AsrService { final bool result = await _channel.invokeMethod('startContinuousRecognition', { 'audioSourceType': audioSourceType, + 'isRemoveFirstPunctuation': isRemoveFirstPunctuation, }); if (!result) { diff --git a/lib/data/services/speech_impl/azure_tts_service.dart b/lib/data/services/speech_impl/azure_tts_service.dart index 369e0ca31..cebcb9a10 100644 --- a/lib/data/services/speech_impl/azure_tts_service.dart +++ b/lib/data/services/speech_impl/azure_tts_service.dart @@ -182,6 +182,14 @@ class AzureTtsService extends GetxService implements TtsService { } } + @override + Future setVoiceFlocking(String speakerProfileId) async { + final result = await _channel.invokeMethod('setVoiceFlocking', { + 'speakerProfileId': speakerProfileId, + }); + return result; + } + @override Future startspeak(String sessionid) async { if (!_isInitialized) await initialize(); diff --git a/lib/data/services/speech_impl/voice_clone_tts_service.dart b/lib/data/services/speech_impl/voice_clone_tts_service.dart index a779270c4..15ea6803b 100644 --- a/lib/data/services/speech_impl/voice_clone_tts_service.dart +++ b/lib/data/services/speech_impl/voice_clone_tts_service.dart @@ -94,6 +94,11 @@ class VoiceCloneTtsService extends GetxService implements TtsService { } } + @override + Future setVoiceFlocking(String speakerProfileId) async { + return true; + } + // 语音合成 Future _speechSynthesis(String text) async { await _tryConnecting(); diff --git a/lib/data/services/speech_impl/volcano_asr_api_service.dart b/lib/data/services/speech_impl/volcano_asr_api_service.dart index 80714696f..3e5711b38 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -862,7 +862,10 @@ class VolcanoAsrApiService implements AsrService { } @override - Future startContinuousRecognition(bool audioSourceType) { + Future startContinuousRecognition( + bool audioSourceType, { + bool isRemoveFirstPunctuation = true, + }) { // TODO: implement startContinuousRecognition throw UnimplementedError(); } diff --git a/lib/data/services/speech_impl/volcano_asr_service.dart b/lib/data/services/speech_impl/volcano_asr_service.dart index 3ed457c3b..ec36a0385 100644 --- a/lib/data/services/speech_impl/volcano_asr_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_service.dart @@ -397,7 +397,10 @@ class VolcanoAsrService extends GetxService implements AsrService { } @override - Future startContinuousRecognition(bool audioSourceType) { + Future startContinuousRecognition( + bool audioSourceType, { + bool isRemoveFirstPunctuation = true, + }) { // TODO: implement startContinuousRecognition throw UnimplementedError(); } diff --git a/lib/data/services/speech_impl/volcano_tts_service.dart b/lib/data/services/speech_impl/volcano_tts_service.dart index b2bdcc2a1..2bb04ade0 100644 --- a/lib/data/services/speech_impl/volcano_tts_service.dart +++ b/lib/data/services/speech_impl/volcano_tts_service.dart @@ -185,6 +185,11 @@ class VolcanoTtsService extends GetxService implements TtsService { } } + @override + Future setVoiceFlocking(String speakerProfileId) async { + return true; + } + /// 设置是否启用音频数据回调 Future setEnableAudioCallback(bool enable) async { if (!await _ensureInitialized()) return false; diff --git a/lib/data/services/speech_impl/xunfei_asr_service.dart b/lib/data/services/speech_impl/xunfei_asr_service.dart index 9dda44c97..284e50519 100644 --- a/lib/data/services/speech_impl/xunfei_asr_service.dart +++ b/lib/data/services/speech_impl/xunfei_asr_service.dart @@ -321,7 +321,10 @@ class XunfeiAsrService extends GetxService implements AsrService { } @override - Future startContinuousRecognition(bool audioSourceType) { + Future startContinuousRecognition( + bool audioSourceType, { + bool isRemoveFirstPunctuation = true, + }) { // TODO: implement startContinuousRecognition throw UnimplementedError(); } diff --git a/lib/data/services/tts_service.dart b/lib/data/services/tts_service.dart index f36827699..b4bf72625 100644 --- a/lib/data/services/tts_service.dart +++ b/lib/data/services/tts_service.dart @@ -19,6 +19,9 @@ abstract class TtsService { /// 设置语音 Future setVoice(String voiceName); + /// 设置语音复刻 + Future setVoiceFlocking(String speakerProfileId); + ///开始新的播放会话 Future startspeak(String sessionid); diff --git a/lib/data/services/usage_stats_service.dart b/lib/data/services/usage_stats_service.dart new file mode 100644 index 000000000..969e2ea55 --- /dev/null +++ b/lib/data/services/usage_stats_service.dart @@ -0,0 +1,783 @@ +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import '../models/translation_usage_stats.dart'; +import '../models/meeting_usage_stats.dart'; +import '../../core/utils/logger.dart'; +import 'network/api.dart'; +import '../models/goods.dart'; +import '../models/user_Info.dart'; + +/// 使用量统计服务 +/// +class UsageStatsService extends GetxService { + static const String _dailyStatsKey = 'daily_translation_stats'; + static const String _meetingDailyKey = 'daily_meeting_asr_stats'; + static const String _meetingOsDailykey = 'daily_meeting_os_stats'; + + final GetStorage _storage = GetStorage(); + + // 实时统计数据 + final Rx realtimeTranslationStats = + RealtimeUsageStats().obs; + + // 当日统计数据 + final Rx todayTranslationStats = + TranslationUsageStats.empty('').obs; + + @override + void onInit() { + super.onInit(); + _initializeService(); + } + + /// 初始化服务 + void _initializeService() { + _loadTodayTranslationStats(); + Logger.info('lxm--使用量统计服务初始化完成'); + } + + /// 加载当日翻译统计数据 + void _loadTodayTranslationStats() { + try { + final today = DateTime.now(); + final todayKey = + '${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}'; + + final statsData = _storage.read('${_dailyStatsKey}_$todayKey'); + if (statsData != null) { + todayTranslationStats.value = TranslationUsageStats.fromJson( + Map.from(statsData)); + } else { + todayTranslationStats.value = + TranslationUsageStats.empty(getCurrentUserId()); + } + } catch (e) { + Logger.error('加载当日翻译统计数据失败: $e'); + todayTranslationStats.value = + TranslationUsageStats.empty(getCurrentUserId()); + } + } + + /// 获取当前用户ID + String getCurrentUserId() { + try { + final userInfo = _storage.read('user_info'); + if (userInfo != null && userInfo['user'] != null) { + return userInfo['user']['uid'] ?? 'anonymous'; + } + } catch (e) { + Logger.warning('获取用户ID失败: $e'); + } + return 'anonymous'; + } + + MeetingAsrDailyStats _loadMeetingDaily(String dateKey) { + final data = _storage.read('${_meetingDailyKey}_$dateKey'); + if (data != null) { + return MeetingAsrDailyStats.fromJson(Map.from(data)); + } + return MeetingAsrDailyStats( + date: dateKey, userId: getCurrentUserId(), items: []); + } + + void _saveMeetingDaily(MeetingAsrDailyStats stats) { + _storage.write('${_meetingDailyKey}_${stats.date}', stats.toJson()); + Logger.d('MeetingASRrecord', + '保存会议当日统计: date=${stats.date}, items=${stats.items.length}'); + } + + String _formatDate(DateTime d) { + final y = d.year; + final m = d.month.toString().padLeft(2, '0'); + final day = d.day.toString().padLeft(2, '0'); + return '$y-$m-$day'; + } + + /// 记录单次对象存储上传 + /// [meetingId] 会议ID + /// [storageUrl] 云端存储URL + /// [storageBytes] 文件大小(字节) + /// [transferBytes] 数据传输字节数 + /// [uploadTimestamp] 上传完成时间戳(毫秒) + void recordMeetingObjectStorage({ + required int meetingId, + required String storageUrl, + required int storageBytes, + required int transferBytes, + required int uploadTimestamp, + }) { + final dateKey = + _formatDate(DateTime.fromMillisecondsSinceEpoch(uploadTimestamp)); + final storageKey = '${_meetingOsDailykey}_$dateKey'; + + final raw = _storage.read(storageKey); + List uploads = []; + if (raw is List) { + uploads = raw + .map((e) => + MeetingObjectStorageRecord.fromJson(Map.from(e))) + .toList(); + } + + // final id = 'os_${uploadTimestamp}_${meetingId}'; + final record = MeetingObjectStorageRecord( + meetingId: meetingId, + storageUrl: storageUrl, + storageBytes: storageBytes, + transferBytes: transferBytes, + uploadTimestamp: uploadTimestamp, + ); + + // 使用 meetingId + storageUrl 作为唯一标识来判断重复 + final exists = uploads.indexWhere( + (e) => e.meetingId == meetingId && e.storageUrl == storageUrl) >= + 0; + if (!exists) { + uploads.add(record); + _storage.write(storageKey, uploads.map((e) => e.toJson()).toList()); + Logger.d('MeetingOSrecord', + '新增对象存储上传: meetingId=$meetingId, size=$storageBytes, transfer=$transferBytes'); + } else { + Logger.d('MeetingOSrecord', + '重复对象存储上传忽略: meetingId=$meetingId, url=$storageUrl'); + } + } + + /// 添加或更新会议ASR记录 + /// [record] 会议ASR记录 + /// 该方法用于记录一次会议语音识别(ASR)的提交记录。它会根据记录的时间戳确定日期, + /// 并将记录添加到该日期的会议ASR记录列表中。如果记录已存在,则会更新该记录。 + void addMeetingAsrRecord(MeetingAsrRecord record) { + final dateKey = _formatDate( + DateTime.fromMillisecondsSinceEpoch(record.submitTimestamp)); + final daily = _loadMeetingDaily(dateKey); + final idx = daily.items.indexWhere((e) => e.id == record.id); + if (idx >= 0) { + daily.items[idx] = record; + Logger.d('MeetingASRrecord', + '更新会议ASR记录: id=${record.id}, meetingId=${record.meetingId}, date=${dateKey}'); + } else { + daily.items.add(record); + Logger.d('MeetingASRrecord', + '新增会议ASR记录: id=${record.id}, meetingId=${record.meetingId}, date=${dateKey}'); + } + _saveMeetingDaily(daily); + Logger.i('MeetingASRrecord', + '会议ASR提交: id=${record.id}, meetingId=${record.meetingId}, audioSeconds=${record.audioSeconds}, fileSizeBytes=${record.fileSizeBytes}, language=${record.language}, date=${dateKey}'); + } + + /// 完成会议ASR记录 + /// [id] 会议ASR记录ID + /// [transcriptBytes] 转写结果文本字节数 + /// [completeTimestamp] 完成时间戳 + /// 该方法用于标记一次会议语音识别(ASR)任务为已完成状态。 + /// 它会根据记录ID在最近3天的会议ASR记录列表中查找匹配项, + /// 如果找到,则更新该记录的状态为已完成,并记录转写结果文本字节数和完成时间戳。 + void completeMeetingAsrRecord({ + required String id, + required int transcriptBytes, + required int completeTimestamp, + }) { + final now = DateTime.now(); + for (int i = 0; i < 3; i++) { + final dateKey = _formatDate(now.subtract(Duration(days: i))); + final daily = _loadMeetingDaily(dateKey); + final idx = daily.items.indexWhere((e) => e.id == id); + if (idx >= 0) { + final old = daily.items[idx]; + daily.items[idx] = MeetingAsrRecord( + id: old.id, + meetingId: old.meetingId, + taskId: old.taskId, + audioSeconds: old.audioSeconds, + fileSizeBytes: old.fileSizeBytes, + language: old.language, + submitTimestamp: old.submitTimestamp, + status: asrStatusCompleted, + transcriptBytes: transcriptBytes, + completeTimestamp: completeTimestamp, + ); + _saveMeetingDaily(daily); + Logger.i('MeetingASRrecord', + '会议ASR完成: id=${id}, meetingId=${old.meetingId}, transcriptBytes=${transcriptBytes}, audioSeconds=${old.audioSeconds}, date=${dateKey}'); + try { + _reportMeetingUsageToServer( + audioSeconds: old.audioSeconds, + audioBytes: old.fileSizeBytes, + language: old.language, + transcriptBytes: transcriptBytes, + ); + } catch (e) { + Logger.error('会议用量上报(完成)失败: $e'); + } + return; + } + } + } + + /// 上报会议用量到服务器 + /// [audioSeconds] 音频时长(秒) + /// [audioBytes] 音频文件大小(字节) + /// [language] 识别语言 + /// [transcriptBytes] 转写结果文本字节数(可选) + /// 该方法用于将会议语音识别(ASR)的用量数据上报到服务器。 + /// 它会根据提供的参数构建一个包含会议用量类型、音频时长、音频文件大小、 + /// 识别语言以及可选的转写结果文本字节数的参数对象。 + /// 然后,它会调用API接口将该参数对象发送到服务器进行处理。 + /// 如果上报成功,服务器会返回一个响应,该方法会根据响应更新本地的用户积分(非总积分)。 + Future _reportMeetingUsageToServer({ + required int audioSeconds, + required int audioBytes, + required String language, + int? transcriptBytes, + }) async { + try { + final params = { + 'usagetype': UsageType.Meet.toInt, + 'usages': { + 'MEETING_ASR_AUDIO_SECONDS': audioSeconds, + // 'MEETING_ASR_AUDIO_BYTES': audioBytes, + // if (transcriptBytes != null) + // 'MEETING_ASR_TRANSCRIPT_BYTES': transcriptBytes, + // 'MEETING_ASR_LANGUAGE_$language': 1, + }, + }; + + Logger.info('lxm---MEET用量上报参数: $params'); + + final response = await Api.usagesResPoints(params); + + Logger.info('lxm---MEET用量上报响应: $response'); + + _applyUsageResponse(response); + } catch (e) { + Logger.error('会议用量上报失败: $e'); + } + } + + /// 记录翻译API调用 + void recordTranslationApiCall({ + required String sourceText, + required String targetText, + required String sourceLanguage, + required String targetLanguage, + required String mode, + }) { + try { + // 更新实时统计 + realtimeTranslationStats.value.translationCount++; + realtimeTranslationStats.value.sourceTextCharacters += sourceText.length; + realtimeTranslationStats.value.ttsTextCharacters += targetText.length; + realtimeTranslationStats.value.currentLanguagePair = + '$sourceLanguage-$targetLanguage'; + realtimeTranslationStats.value.currentMode = mode; + realtimeTranslationStats.refresh(); + + // 更新当日统计 - 使用多维度统计 + final languagePair = '$sourceLanguage→$targetLanguage'; + + // 更新语言对统计 + final newLanguagePairStats = Map.from( + todayTranslationStats.value.languagePairStats); + if (newLanguagePairStats.containsKey(languagePair)) { + final existing = newLanguagePairStats[languagePair]!; + newLanguagePairStats[languagePair] = LanguagePairStats( + languagePair: languagePair, + translationCount: existing.translationCount + 1, + sourceCharacters: existing.sourceCharacters + sourceText.length, + ); + } else { + newLanguagePairStats[languagePair] = LanguagePairStats( + languagePair: languagePair, + translationCount: 1, + sourceCharacters: sourceText.length, + ); + } + + // 更新模式统计 + final newModeStats = Map.from( + todayTranslationStats.value.modeStats); + if (newModeStats.containsKey(mode)) { + final existing = newModeStats[mode]!; + newModeStats[mode] = existing.copyWith( + apiCalls: existing.apiCalls + 1, + sourceCharacters: existing.sourceCharacters + sourceText.length, + ttsCharacters: existing.ttsCharacters + targetText.length, + ); + } else { + newModeStats[mode] = TranslationModeStats( + apiCalls: 1, + sourceCharacters: sourceText.length, + ttsCharacters: targetText.length, + ); + } + + todayTranslationStats.value = todayTranslationStats.value.copyWith( + translationApiCalls: + todayTranslationStats.value.translationApiCalls + 1, + sourceTextCharacters: todayTranslationStats.value.sourceTextCharacters + + sourceText.length, + ttsTextCharacters: + todayTranslationStats.value.ttsTextCharacters + targetText.length, + languagePairStats: newLanguagePairStats, + modeStats: newModeStats, + ); + + _saveTodayStats(); + + Logger.info( + '记录翻译API调用: 源文本${sourceText.length}字符, 目标文本${targetText.length}字符'); + } catch (e) { + Logger.error('记录翻译API调用失败: $e'); + } + } + + /// 记录ASR API调用 - 优化版(精确统计参数) + void recordAsrApiCall({ + required int effectiveDurationSeconds, // 有效语音时长(VAD检测的实际语音) + required int totalDurationSeconds, // 总会话时长(包含静音) + required String language, + required String mode, // 翻译模式 + String? audioFormat, // 音频格式(可选) + int? sampleRate, // 采样率(可选) + }) { + try { + // 更新实时统计(使用有效语音时长作为主要计费依据) + realtimeTranslationStats.value.effectiveAudioDuration += + effectiveDurationSeconds; + realtimeTranslationStats.value.totalSessionDuration += + totalDurationSeconds; + realtimeTranslationStats.refresh(); + + // 更新模式统计 + final newModeStats = Map.from( + todayTranslationStats.value.modeStats); + if (newModeStats.containsKey(mode)) { + final existing = newModeStats[mode]!; + newModeStats[mode] = existing.copyWith( + effectiveAudioDuration: + existing.effectiveAudioDuration + effectiveDurationSeconds, + ); + } else { + newModeStats[mode] = TranslationModeStats( + effectiveAudioDuration: effectiveDurationSeconds, + ); + } + + // 更新当日统计 + todayTranslationStats.value = todayTranslationStats.value.copyWith( + asrApiCalls: todayTranslationStats.value.asrApiCalls + 1, + effectiveAudioDuration: + todayTranslationStats.value.effectiveAudioDuration + + effectiveDurationSeconds, + totalSessionDuration: todayTranslationStats.value.totalSessionDuration + + totalDurationSeconds, + modeStats: newModeStats, + ); + + _saveTodayStats(); + + Logger.info( + 'ASR统计: 有效语音${effectiveDurationSeconds}秒, 总时长${totalDurationSeconds}秒, 语言$language, 模式$mode'); + } catch (e) { + Logger.error('记录ASR API调用失败: $e'); + } + + _reportAsrUsageToServer( + effectiveDurationSeconds: effectiveDurationSeconds, + //totalDurationSeconds: totalDurationSeconds, + language: language, + mode: mode, + ); + } + + /// 上报ASR用量到服务器 + /// [effectiveDurationSeconds] 有效语音时长(秒) + /// [language] 识别语言 + /// [mode] 翻译模式 + /// 该方法用于将ASR(语音识别)的用量数据上报到服务器。 + /// 它会根据提供的参数构建一个包含用量类型、有效语音时长、识别语言以及翻译模式的参数对象。 + /// 然后,它会调用API接口将该参数对象发送到服务器进行处理。 + /// 如果上报成功,服务器会返回一个响应,该方法会根据响应更新本地的用户积分(非总积分)。 + Future _reportAsrUsageToServer({ + required int effectiveDurationSeconds, + //required int totalDurationSeconds, + required String language, + required String mode, + }) async { + try { + final params = { + 'usagetype': UsageType.Translation.toInt, + 'usages': { + // 'TRANSLATION_ASR_EFFECTIVE_SECONDS': effectiveDurationSeconds, + //'TRANSLATION_ASR_TOTAL_SECONDS': totalDurationSeconds, + 'TRANSLATION_MODE_$mode': effectiveDurationSeconds, + 'TRANSLATION_ASR_LANGUAGE_$language': 1, + // 'TRANSLATION_SOURCE_CHARACTERS': + // realtimeTranslationStats.value.sourceTextCharacters, + // 'TRANSLATION_TARGET_CHARACTERS': + // realtimeTranslationStats.value.ttsTextCharacters, + }, + }; + + Logger.info('lxm---ASR用量上报参数: $params'); + + final response = await Api.usagesResPoints(params); + + Logger.info('lxm---ASR用量上报响应: $response'); + + _applyUsageResponse(response); + } catch (e) { + Logger.error('记录ASR API调用失败: $e'); + } + } + + /// 根据服务器用量上报回应,更新用户积分(非总积分) + /// 用量类型:0 AI聊天、1 翻译、2 会议 + void _applyUsageResponse(dynamic response) { + try { + if (response is! Map) return; + final usagesObj = response['usages']; + if (usagesObj is! Map) return; + final usages = Map.from(usagesObj as Map); + int _asInt(dynamic v) { + if (v == null) return 0; + if (v is int) return v; + if (v is num) return v.toInt(); + if (v is String) return int.tryParse(v) ?? 0; + return 0; + } + + final Map update = {}; + usages.forEach((k, v) { + final key = int.tryParse(k.toString()); + final val = _asInt(v); + if (key == null) return; + switch (key) { + case 0: + update['aichatintegral'] = val; + break; + case 1: + update['tradeintegral'] = val; + break; + case 2: + update['meetintegral'] = val; + break; + } + }); + if (update.isNotEmpty) { + User.instance.updateUserInfo(update); + Logger.info('积分更新: $update'); + } + } catch (e) { + Logger.error('解析用量回应并更新积分失败: $e'); + } + } + + /// 记录TTS API调用 + void recordTtsApiCall({ + required String text, + required int durationSeconds, + required String language, + }) { + try { + // 更新实时统计 + realtimeTranslationStats.value.totalSessionDuration += durationSeconds; + realtimeTranslationStats.refresh(); + + // 更新当日统计(TTS按字符数计费,不是按时长) + todayTranslationStats.value = todayTranslationStats.value.copyWith( + ttsApiCalls: todayTranslationStats.value.ttsApiCalls + 1, + ttsTextCharacters: + todayTranslationStats.value.ttsTextCharacters + text.length, + totalSessionDuration: + todayTranslationStats.value.totalSessionDuration + durationSeconds, + ); + + _saveTodayStats(); + + Logger.info('记录TTS API调用: 文本${text.length}字符, 时长${durationSeconds}秒'); + } catch (e) { + Logger.error('记录TTS API调用失败: $e'); + } + } + + /// 记录AI摘要API调用 + void recordAiSummaryApiCall() { + try { + // 更新当日统计 + todayTranslationStats.value = todayTranslationStats.value.copyWith( + aiSummaryApiCalls: todayTranslationStats.value.aiSummaryApiCalls + 1, + ); + + _saveTodayStats(); + + Logger.info('记录AI摘要API调用'); + } catch (e) { + Logger.error('记录AI摘要API调用失败: $e'); + } + } + + /// 开始新会话 + void startNewSession() { + realtimeTranslationStats.value.reset(); + + // 更新当日会话数 + todayTranslationStats.value = todayTranslationStats.value.copyWith( + sessionCount: todayTranslationStats.value.sessionCount + 1, + ); + + _saveTodayStats(); + Logger.info('开始新的翻译会话'); + } + + /// 保存当日统计数据 + void _saveTodayStats() { + try { + final today = DateTime.now(); + final todayKey = + '${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}'; + _storage.write( + '${_dailyStatsKey}_$todayKey', todayTranslationStats.value.toJson()); + } catch (e) { + Logger.error('保存当日统计数据失败: $e'); + } + } + + /// 获取历史统计数据 + Future> getHistoryStats({ + required DateTime startDate, + required DateTime endDate, + }) async { + try { + final List historyStats = []; + + for (DateTime date = startDate; + date.isBefore(endDate) || date.isAtSameMomentAs(endDate); + date = date.add(const Duration(days: 1))) { + final dateKey = + '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + final statsData = _storage.read('${_dailyStatsKey}_$dateKey'); + + if (statsData != null) { + historyStats.add(TranslationUsageStats.fromJson( + Map.from(statsData))); + } + } + + return historyStats; + } catch (e) { + Logger.error('获取历史统计数据失败: $e'); + return []; + } + } + + /// 清理过期数据(保留最近90天) + void cleanupOldData() { + try { + final cutoffDate = DateTime.now().subtract(const Duration(days: 90)); + final allKeys = _storage.getKeys(); + + for (final key in allKeys) { + if (key.startsWith(_dailyStatsKey)) { + final dateStr = key.replaceFirst('${_dailyStatsKey}_', ''); + try { + final date = DateTime.parse(dateStr); + if (date.isBefore(cutoffDate)) { + _storage.remove(key); + Logger.info('清理过期统计数据: $key'); + } + } catch (e) { + // 忽略解析错误的键 + } + } + } + } catch (e) { + Logger.error('清理过期数据失败: $e'); + } + } + + /// 获取月度统计汇总 + Future getMonthlyStats(int year, int month) async { + try { + final startDate = DateTime(year, month, 1); + final endDate = DateTime(year, month + 1, 0); // 月末 + + final dailyStats = await getHistoryStats( + startDate: startDate, + endDate: endDate, + ); + + if (dailyStats.isEmpty) { + return TranslationUsageStats.empty(getCurrentUserId()); + } + + // 汇总月度数据 + int totalTranslationCalls = 0; + int totalAsrCalls = 0; + int totalTtsCalls = 0; + int totalAiSummaryCalls = 0; + int totalSourceChars = 0; + int totalTargetChars = 0; + int totalRecordingDuration = 0; + int totalTtsPlayDuration = 0; + int totalSessions = 0; + //double totalCost = 0.0; + + final Map combinedLanguagePairUsage = {}; + final Map combinedModeUsage = {}; + + for (final stats in dailyStats) { + totalTranslationCalls += stats.translationApiCalls; + totalAsrCalls += stats.asrApiCalls; + totalTtsCalls += stats.ttsApiCalls; + totalAiSummaryCalls += stats.aiSummaryApiCalls; + totalSourceChars += stats.sourceCharacterCount; + totalTargetChars += stats.targetCharacterCount; + totalRecordingDuration += stats.audioRecordingDuration; + totalTtsPlayDuration += stats.ttsPlaybackDuration; + totalSessions += stats.sessionCount; + // totalCost += stats.estimatedCost; + + // 合并语言对使用统计 + stats.languagePairUsage.forEach((key, value) { + combinedLanguagePairUsage[key] = + (combinedLanguagePairUsage[key] ?? 0) + value; + }); + + // 合并模式使用统计 + stats.modeUsage.forEach((key, value) { + combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value; + }); + } + + return TranslationUsageStats( + id: 'monthly_${year}_$month', + date: startDate, + userId: getCurrentUserId(), + timeGranularity: 'month', + translationApiCalls: totalTranslationCalls, + asrApiCalls: totalAsrCalls, + ttsApiCalls: totalTtsCalls, + aiSummaryApiCalls: totalAiSummaryCalls, + sourceTextCharacters: totalSourceChars, + effectiveAudioDuration: totalRecordingDuration, + ttsTextCharacters: totalTargetChars, + totalSessionDuration: totalTtsPlayDuration, + sessionCount: totalSessions, + // estimatedCost: totalCost, + ); + } catch (e) { + Logger.error('获取月度统计失败: $e'); + return TranslationUsageStats.empty(getCurrentUserId()); + } + } + + /// 获取会议历史统计 + /// + /// 从[startDate]到[endDate]的会议ASR记录。 + Future> getMeetingHistoryStats({ + required DateTime startDate, + required DateTime endDate, + }) async { + try { + final List list = []; + for (DateTime date = startDate; + date.isBefore(endDate) || date.isAtSameMomentAs(endDate); + date = date.add(const Duration(days: 1))) { + final key = _formatDate(date); + final daily = _loadMeetingDaily(key); + if (daily.items.isNotEmpty) { + list.add(daily); + } + } + Logger.i('MeetingASRrecord', + '获取会议历史统计: start=${_formatDate(startDate)}, end=${_formatDate(endDate)}, 有效天数=${list.length}'); + return list; + } catch (e) { + Logger.error('获取会议历史统计失败: $e'); + return []; + } + } + + /// 聚合会议统计 + /// + /// 从[daily]中聚合会议统计数据。 + MeetingAsrHistoryStats aggregateMeetingStats( + List daily) { + int calls = 0; + int audioSeconds = 0; + int audioBytes = 0; + int transcriptBytes = 0; + for (final d in daily) { + calls += d.asrCalls; + audioSeconds += d.audioSecondsTotal; + audioBytes += d.audioBytesTotal; + transcriptBytes += d.transcriptBytesTotal; + } + Logger.i('MeetingASRrecord', + '聚合会议统计: calls=${calls}, audioSeconds=${audioSeconds}, audioBytes=${audioBytes}, transcriptBytes=${transcriptBytes}'); + return MeetingAsrHistoryStats( + userId: getCurrentUserId(), + allAsrCalls: calls, + allAudioSeconds: audioSeconds, + allAudioFileBytes: audioBytes, + allTranscriptBytes: transcriptBytes, + ); + } + + /// 获取会议对象存储历史统计 + /// + /// 从[startDate]到[endDate]的会议对象存储上传记录。 + Future> getMeetingStorageHistoryStats({ + required DateTime startDate, + required DateTime endDate, + }) async { + try { + final List list = []; + for (DateTime date = startDate; + date.isBefore(endDate) || date.isAtSameMomentAs(endDate); + date = date.add(const Duration(days: 1))) { + final key = _formatDate(date); + final raw = _storage.read('${_meetingOsDailykey}_$key'); + List uploads = []; + if (raw is List) { + uploads = raw + .map((e) => MeetingObjectStorageRecord.fromJson( + Map.from(e))) + .toList(); + } + if (uploads.isNotEmpty) { + list.add(MeetingStorageDailyStats( + date: key, userId: getCurrentUserId(), items: uploads)); + } + } + Logger.i('MeetingOSrecord', + '获取会议对象存储历史: start=${_formatDate(startDate)}, end=${_formatDate(endDate)}, 有效天数=${list.length}'); + return list; + } catch (e) { + Logger.error('获取对象存储历史统计失败: $e'); + return []; + } + } + + MeetingStorageHistoryStats aggregateMeetingStorageStats( + List daily) { + int uploads = 0; + int storageBytes = 0; + int transferBytes = 0; + for (final d in daily) { + uploads += d.storageUploads; + storageBytes += d.storageBytesTotal; + transferBytes += d.transferBytesTotal; + } + Logger.i('MeetingOSrecord', + '聚合对象存储统计: uploads=${uploads}, storageBytes=${storageBytes}, transferBytes=${transferBytes}'); + return MeetingStorageHistoryStats( + userId: getCurrentUserId(), + allStorageUploads: uploads, + allStorageBytes: storageBytes, + allTransferBytes: transferBytes, + ); + } +} diff --git a/lib/data/services/version_update_service.dart b/lib/data/services/version_update_service.dart index ac65871e8..b25f861b2 100644 --- a/lib/data/services/version_update_service.dart +++ b/lib/data/services/version_update_service.dart @@ -26,14 +26,15 @@ class VersionUpdateService extends GetxService { static String? _updateUrl = ""; //更新链接 static AppInfo? _appInfo; //应用信息 static bool? _isShowTourists = false; //是否显示游客登录 + static bool? _allowSkipDeviceBinding = false; //是否允许跳过设备绑定 static DateTime? _lastCheckTime; //上次检查时间 static const int _cacheIntervalMinutes = 10; // 可配置化 static final String? _countryCode = Get.deviceLocale?.countryCode; //国家地区编码 static final String? _languageCode = Get.deviceLocale?.languageCode; //语言编码 // ignore: non_constant_identifier_names - static final String? _SERVER_URL = (_countryCode == 'CN') - ? dotenv.env['SERVER_URL'] - : dotenv.env['HW_SERVER_URL']; //服务器地址 + // static final String? _SERVER_URL = (_countryCode == 'CN') + // ? dotenv.env['SERVER_URL'] + // : dotenv.env['HW_SERVER_URL']; //服务器地址 // 添加 getter 方法来访问缓存的静态数据 /// 获取缓存的应用版本 @@ -73,6 +74,9 @@ class VersionUpdateService extends GetxService { /// 获取缓存的游客登录状态 bool get cachedShowTourists => _isShowTourists ?? false; + /// 获取缓存的是否允许跳过设备绑定 + bool get cachedAllowSkipDeviceBinding => _allowSkipDeviceBinding ?? false; + /// 获取上次检查时间 DateTime? get lastCheckTime => _lastCheckTime; @@ -83,7 +87,7 @@ class VersionUpdateService extends GetxService { String get cachedLanguageCode => _languageCode ?? ''; /// 获取缓存的服务器地址 - String get cachedServerUrl => _SERVER_URL ?? ''; + // String get cachedServerUrl => _SERVER_URL ?? ''; final Logger _logger = Logger(); @@ -120,15 +124,15 @@ class VersionUpdateService extends GetxService { //_channel = platformInfo.channel; _lastCheckTime = DateTime.now(); - // 调用后端接口获取最新版本信息 - final response = - await Api.getChannelApp({'channel': platformInfo.channel.value}); + // 获取所有渠道包信息 + final response = await Api.getChannelApps(); + + _logger.d('lxm--获取所有渠道包信息:$response'); - _logger.d('lxm--获取渠道包信息:$response'); + final List apps = _parseAppsList(response); - if (response == null || !response.containsKey('app')) { - //[2]获取渠道包信息失败: 响应为空或不包含app字段 - _logger.w('lxm--获取渠道包信息失败: 响应为空或不包含app字段'); + if (apps.isEmpty) { + _logger.w('lxm--获取所有渠道包信息失败或为空'); return VersionUpdateInfo( hasUpdate: false, currentVersion: currentVersion, @@ -137,22 +141,38 @@ class VersionUpdateService extends GetxService { ); } - //final channelAppResponse = ChannelAppResponse.fromJson(response); - // 直接从response中获取app信息 - final appInfo = AppInfo.fromJson(response['app']); - _appInfo = appInfo; - _logger.d('获取到的最新版本信息: ${appInfo.toJson()}'); - _latestVersion = appInfo.version; - _updateUrl = appInfo.address; + final Map appIndex = _buildChannelIndex(apps); - // 设置游客登录显示状态(先用当前渠道结果作为回退,再用.env渠道覆盖以保持一致) - _isShowTourists = appInfo.tourists; - //print('lxm--_isShowTourists: $_isShowTourists'); - if (Platform.isAndroid && _channel != platformInfo.channel) { - //[4]当前渠道与.env渠道不同,需要从.env渠道获取游客登录配置 - await _refreshTouristsFromEnv(); + // 依据设备品牌渠道用于版本更新链接 + final brandChannel = platformInfo.channel; + + // 环境变量渠道用于游客登录配置 + final envChannelName = dotenv.env['Channel'] ?? 'OFFICIAL'; + final envChannel = UserChannel.fromName(envChannelName); + final AppInfo? envAppInfo = appIndex[envChannel]; + + // 若不存在品牌渠道信息,降级为环境渠道;仍不存在则返回无更新 + final AppInfo? chosenAppInfo = + _getChannelAppInfo(appIndex, brandChannel, envChannel); + if (chosenAppInfo == null) { + _logger.w('lxm--未找到品牌或环境渠道的应用信息,返回无更新'); + return VersionUpdateInfo( + hasUpdate: false, + currentVersion: currentVersion, + latestVersion: currentVersion, + updateType: UpdateType.none, + ); } + _appInfo = chosenAppInfo; + _latestVersion = chosenAppInfo.version; + _updateUrl = chosenAppInfo.address; + + // 设置游客登录显示状态(先用当前渠道结果作为回退,再用.env渠道覆盖以保持一致) + _isShowTourists = (envAppInfo ?? chosenAppInfo).tourists; + _allowSkipDeviceBinding = + (envAppInfo ?? chosenAppInfo).allowskipdevicebinding; + _logger.d('lxm--从服务器获取游客登录配置(最终): ${_isShowTourists}'); // if (channelAppResponse.code != 0 || channelAppResponse.data == null) { @@ -166,20 +186,21 @@ class VersionUpdateService extends GetxService { // } // final appInfo = channelAppResponse.data!.app; - _hasUpdate = _compareVersions(currentVersion, appInfo.version); + _hasUpdate = _compareVersions(currentVersion, chosenAppInfo.version); // 判断更新类型:如果主版本号不同则强制更新 UpdateType updateType = UpdateType.none; if (_hasUpdate!) { - _isForceUpdate = _shouldForceUpdate(currentVersion, appInfo.version); + _isForceUpdate = + _shouldForceUpdate(currentVersion, chosenAppInfo.version); updateType = _isForceUpdate! ? UpdateType.force : UpdateType.optional; } //[3]返回版本更新信息 return VersionUpdateInfo( hasUpdate: _hasUpdate!, currentVersion: currentVersion.split('+')[0], - latestVersion: appInfo.version.split('+')[0], - appInfo: appInfo, + latestVersion: chosenAppInfo.version.split('+')[0], + appInfo: chosenAppInfo, updateType: updateType, ); } catch (e) { @@ -289,30 +310,6 @@ class VersionUpdateService extends GetxService { } } - /// 从.env文件刷新游客登录配置 - Future _refreshTouristsFromEnv() async { - try { - final envChannelName = dotenv.env['Channel']; - if (envChannelName == null || envChannelName.isEmpty) { - _logger.w('未配置.env Channel,沿用当前渠道的游客配置: ${_isShowTourists}'); - return; - } - final envChannel = UserChannel.fromName(envChannelName); - - final resp = await Api.getChannelApp({'channel': envChannel.value}); - if (resp != null && resp.containsKey('app')) { - final envAppInfo = AppInfo.fromJson(resp['app']); - _isShowTourists = envAppInfo.tourists; - _logger.d( - 'lxm--游客登录配置已按.env渠道同步: ${envAppInfo.tourists} (channel=$envChannelName),_isShowTourists: $_isShowTourists'); - } else { - _logger.w('按.env渠道获取游客配置失败:响应为空或不含app字段'); - } - } catch (e) { - _logger.w('按.env渠道同步游客配置失败: $e'); - } - } - /// 获取iOS设备品牌信息 Future _getIosDeviceBrand() async { try { @@ -405,6 +402,45 @@ class VersionUpdateService extends GetxService { return latestVersions.length > currentVersions.length; } + /// 解析响应数据为AppInfo列表 + List _parseAppsList(dynamic response) { + try { + final appsJson = (response is Map) ? response['apps'] : null; + if (appsJson is List) { + return appsJson + .whereType>() + .map((e) => AppInfo.fromJson(e)) + .toList(); + } + } catch (e) { + _logger.e('解析渠道列表失败: $e'); + } + return []; + } + + /// 构建渠道索引 + Map _buildChannelIndex(List apps) { + final index = {}; + for (final app in apps) { + index[app.channel] = app; + } + return index; + } + + /// 获取渠道信息,带备选策略 + /// + /// 优先返回主渠道的信息,如果主渠道不存在则返回备选渠道的信息。 + /// 主要用于: + /// 1. 版本更新:优先使用设备品牌渠道,备选使用环境变量渠道 + /// 2. 功能配置:优先使用环境变量渠道,备选使用版本更新渠道 + AppInfo? _getChannelAppInfo( + Map index, + UserChannel primary, + UserChannel? fallback, + ) { + return index[primary] ?? (fallback != null ? index[fallback] : null); + } + /// 解析版本号字符串为数字数组 List _parseVersion(String version) { final parts = version.split('.'); diff --git a/lib/data/services/volcano_translation_service.dart b/lib/data/services/volcano_translation_service.dart index 093f58a4b..1ce33d71a 100644 --- a/lib/data/services/volcano_translation_service.dart +++ b/lib/data/services/volcano_translation_service.dart @@ -165,7 +165,12 @@ class VolcanoTranslationService extends GetxService { }; } - // 翻译文本 + // 文本翻译 + /// [text] 待翻译的文本 + /// [sourceLanguageCode] 源语言的ASR代码 + /// [targetLanguageCode] 目标语言的ASR代码 + /// [return] 翻译后的文本 + /// 计费标准:每月200万字符免费,超出部分按49元/百万字符计费 Future translateText({ required String text, required String sourceLanguageCode, @@ -417,6 +422,10 @@ class VolcanoTranslationService extends GetxService { } /// 图片翻译(整图识别+回贴翻译) + /// [imageBytes] 待翻译的图片二进制数据 + /// [targetLanguageCode] 目标语言的ASR代码 + /// [return] 翻译结果 + /// 计费标准:每月100张免费,超出部分按0.04元/张计费 Future translateImage({ required Uint8List imageBytes, required String targetLanguageCode, // ASR代码,如 zh-CN / en-US diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 30b0c8ac3..b124cdbef 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -13,6 +13,7 @@ import '/data/services/bluetooth_manager.dart'; import '/data/services/qq_music.dart'; import 'package:flutter_intro/flutter_intro.dart'; import '../../../data/services/novice_guide_service.dart'; +import '../../../routes/app_routes.dart'; import '/core/utils/upload_oss.dart'; import '/data/models/user_Info.dart'; import '/data/services/music_manager.dart'; @@ -30,6 +31,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:photo_view/photo_view.dart'; import '../../../core/utils/permission_util.dart'; import '../../../data/models/message_model.dart'; +import '../../common/widgets/recharge_dialog.dart'; // 语音输入模式枚举 enum VoiceInputMode { @@ -45,7 +47,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { // 聊天消息列表 final messages = [].obs; final currsessionid = ''.obs; - + final vipExpiryTime = 0.obs; // UI控制器 final TextEditingController textController = TextEditingController(); final ScrollController scrollController = ScrollController(); @@ -165,6 +167,10 @@ class AgentController extends GetxController with WidgetsBindingObserver { @override void onInit() { super.onInit(); + vipExpiryTime.value = User.isLoggedIn() ? User.instance.vipexptime : 0; + ever(User.refreshTick, (_) { + vipExpiryTime.value = User.instance.vipexptime; + }); _initAppDir(); _loadTtsPreference(); //加载TTS偏好设置 _loadTtsInterrupe(); //加载TTS播报打断偏好设置 @@ -550,7 +556,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { messages.add(Message( sessionid: "", // 会话ID isUser: false, - text: 'welcomeMessage'.tr, // 你好!我是**小言**,我能帮你: + text: 'welcomeMessage'.tr, // 你好!我是**Lily**,我能帮你: )); } @@ -626,15 +632,15 @@ class AgentController extends GetxController with WidgetsBindingObserver { final text = event.data['text'] ?? ''; Logger.i(TAG, '[$sessionid]识别最终结果: $text'); - if (currsessionid.value != "" && currsessionid.value != sessionid) { - final index = messages.lastIndexWhere( - (msg) => msg.sessionid == currsessionid.value && msg.isUser); - //出现了为识别结束的消息 - if (index >= 0) { - //异除去当前会话的识别中消息 - messages.removeAt(index); - } - } + // if (currsessionid.value != "" && currsessionid.value != sessionid) { + // final index = messages.lastIndexWhere( + // (msg) => msg.sessionid == currsessionid.value && msg.isUser); + // //出现了为识别结束的消息 + // if (index >= 0) { + // //异除去当前会话的识别中消息 + // messages.removeAt(index); + // } + // } currsessionid.value = sessionid; // 查找是否有正在识别中的消息 final index = messages.lastIndexWhere( @@ -781,9 +787,9 @@ class AgentController extends GetxController with WidgetsBindingObserver { case AgentServiceEventType.error: final sessionid = event.data['sessionid'] ?? ''; final message = event.data['message'] ?? ''; - + final code = event.data['code'] ?? 0; // //验证错误码 - if (event.data['code'] == 1000) { + if (code == 1000) { isListening.value = false; isSpeaking.value = false; isProcessing.value = false; @@ -797,6 +803,10 @@ class AgentController extends GetxController with WidgetsBindingObserver { } } Get.snackbar('error'.tr, '${event.data['message']}'); + } else if (code == 2001) { + // 余额不足或需要充值,给出弹窗确认是否跳转充值界面 + + showRechargeDialog('agent'); } Logger.e(TAG, '代理服务错误: ${event.data['message']}'); break; @@ -1019,6 +1029,11 @@ class AgentController extends GetxController with WidgetsBindingObserver { // } } break; + case AgentServiceEventType.assistantUsages: + final sessionid = event.data['sessionid'] ?? ''; + final integral = event.data['integral'] ?? 0; + User.instance.aichatintegral = integral; + break; default: break; } diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 5aabbb9b1..cfe91e808 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -6,12 +6,14 @@ import 'package:flutter_intro/flutter_intro.dart'; import 'package:get/get.dart'; import 'package:haptic_feedback/haptic_feedback.dart'; import '../../../core/widgets/intro_overlay_builder.dart'; +import '../../../data/models/user_Info.dart'; import '../controllers/agent_controller.dart'; import '../controllers/message_action_controller.dart'; import 'message_bubble.dart'; import '../widgets/full_screen_editor.dart'; import '../widgets/sound_bar_widget.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:intl/intl.dart'; class AgentView extends StatefulWidget { const AgentView({Key? key}) : super(key: key); @@ -90,7 +92,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { ], ), child: Icon( - Icons.smart_toy_outlined, + Icons.support_agent, color: Colors.white, size: 22.sp, ), @@ -100,7 +102,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'XiaoYan'.tr, + 'Lily'.tr, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.bold, @@ -118,14 +120,27 @@ class _AgentViewState extends State with WidgetsBindingObserver { ), ), SizedBox(width: 4.w), - Text( - 'online'.tr, - style: TextStyle( - fontSize: 12.sp, - color: Colors.green, - fontWeight: FontWeight.w500, - ), - ), + Obx(() { + final vipexptime = controller.vipExpiryTime.value; + DateTime? expiry; + if (vipexptime > 0) { + expiry = vipexptime > 1000000000000 + ? DateTime.fromMillisecondsSinceEpoch(vipexptime) + : DateTime.fromMillisecondsSinceEpoch( + vipexptime * 1000); + } + final text = expiry != null + ? '${'expiryLabel'.tr}${DateFormat('yyyy-MM-dd').format(expiry)}' + : 'notActivated'.tr; + return Text( + text, + style: TextStyle( + fontSize: 12.sp, + color: Colors.green, + fontWeight: FontWeight.w500, + ), + ); + }), ], ), ], @@ -275,8 +290,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { params, false, title: controller.newAIReplyCompleted.value - ? '小言已经完成了回答!' - : '小言正在思考中~', + ? 'aiReplyCompleted'.tr + : 'aiThinking'.tr, onNext: controller.newAIReplyCompleted.value ? () {} : null, ), ); @@ -327,9 +342,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, - controller.noviceGuideService.aiStepsList[ - controller.noviceGuideService - .aiStep]['end'], + '${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['endKey']}' + .tr, onFinish: () async { if (controller .noviceGuideService.aiStep < @@ -620,24 +634,18 @@ class _AgentViewState extends State with WidgetsBindingObserver { overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, - controller - .noviceGuideService.aiStepsList[ - controller.noviceGuideService - .aiStep]['start'], - buttonText: '确定输入', + '${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['startKey']}' + .tr, + buttonText: 'confirmInput'.tr, onNext: () { controller.textController.text = - controller.noviceGuideService - .aiStepsList[ - controller.noviceGuideService - .aiStep]['input']; + '${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['inputKey']}' + .tr; }, onTap: () { controller.textController.text = - controller.noviceGuideService - .aiStepsList[ - controller.noviceGuideService - .aiStep]['input']; + '${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['inputKey']}' + .tr; params.onNext!(); }, ); @@ -861,7 +869,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, - '点击发送,${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['send']}', + '${'guideClickSendPrefix'.tr}${'${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['sendKey']}'.tr}', alignment: false, onTap: () async { await controller diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index 7a845e44c..ae545ae1f 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -166,6 +166,7 @@ class MessageBubble extends GetView { // 如果是识别中的消息,添加特殊样式 if (isRecognizing) { return Row( + crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ Flexible( @@ -177,7 +178,9 @@ class MessageBubble extends GetView { fontSize: 14, height: 1.4, ), - overflow: TextOverflow.ellipsis, + // 允许自动换行,不再强制单行 + softWrap: true, + maxLines: null, ), ), const SizedBox(width: 8), diff --git a/lib/modules/common/widgets/language_selector_bar.dart b/lib/modules/common/widgets/language_selector_bar.dart index 495a01a00..4b7878287 100644 --- a/lib/modules/common/widgets/language_selector_bar.dart +++ b/lib/modules/common/widgets/language_selector_bar.dart @@ -58,7 +58,7 @@ class LanguageSelectorBar extends StatelessWidget { isDarkMode ? AppColors.primary.withOpacity(0.2) : Colors.blue[50]; final swapIconColor = isDarkMode ? AppColors.primary : Colors.blue; - // 规范化特殊项的翻译键,例如:自动检测 -> autoDetect(翻译键代表的中文文本内容) + // 规范化特殊项的翻译键,例如:自动检测 -> autoDetect(对应中文:自动检测) String _normalizeKey(String text) { switch (text) { case '自动检测': diff --git a/lib/modules/common/widgets/recharge_dialog.dart b/lib/modules/common/widgets/recharge_dialog.dart new file mode 100644 index 000000000..b3f6d15b8 --- /dev/null +++ b/lib/modules/common/widgets/recharge_dialog.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../routes/app_routes.dart'; + +/// 显示充值弹窗 +/// [type] 充值类型,可选值为 'agent'、'translation'、'meeting' +/// 分别跳转对应ai充值、翻译充值、会议充值界面 +void showRechargeDialog(String type) { + Get.dialog( + Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'tip'.tr, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Text( + (type == 'agent' + ? 'aiMemberExpiredDesc' + : 'insufficientBalanceDesc') + .tr, // 会员过期描述 / 余额不足描述 + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orangeAccent, + foregroundColor: Colors.white, + ), + onPressed: () { + Get.back(); + if (type == 'agent') { + Get.toNamed(Routes.goodsvip); + } else if (type == 'translation') { + Get.toNamed(Routes.goodstrans); + } else if (type == 'meeting') { + Get.toNamed(Routes.goodsmeet); + } + }, + child: Text('recharge'.tr), + ), + ), + ], + ), + ), + Positioned( + right: 8, + top: 8, + child: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Get.back(), + ), + ), + ], + ), + ), + barrierDismissible: true, + ); +} diff --git a/lib/modules/devices/controllers/devices_controller.dart b/lib/modules/devices/controllers/devices_controller.dart index 1b88dfd0e..afca3899d 100644 --- a/lib/modules/devices/controllers/devices_controller.dart +++ b/lib/modules/devices/controllers/devices_controller.dart @@ -1,4 +1,4 @@ -import 'dart:ffi'; +// import 'dart:ffi'; // Not supported on web; not needed here import '/data/models/appconfig_model.dart'; diff --git a/lib/modules/devices/views/devices_view.dart b/lib/modules/devices/views/devices_view.dart index 846004a4e..7a69dd5b0 100644 --- a/lib/modules/devices/views/devices_view.dart +++ b/lib/modules/devices/views/devices_view.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import '../../../routes/app_routes.dart'; import '/modules/devices/controllers/devices_controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -201,103 +202,107 @@ class DevicesView extends GetView { required DeviceType deviceType, required bool isDarkMode, }) { - return Container( - margin: EdgeInsets.only(bottom: 12.h), - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - children: [ - // 设备图标 - Container( - width: 48.w, - height: 48.w, - decoration: BoxDecoration( - color: - isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], - borderRadius: BorderRadius.circular(12.r), + return GestureDetector( + onTap: () => Get.toNamed(Routes.eqSettings), + child: Container( + margin: EdgeInsets.only(bottom: 12.h), + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, 2), ), - child: Icon( - deviceType.icon, - color: isDarkMode ? Colors.white : Colors.black87, - size: 24.sp, + ], + ), + child: Row( + children: [ + // 设备图标 + Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[100], + borderRadius: BorderRadius.circular(12.r), + ), + child: Icon( + deviceType.icon, + color: isDarkMode ? Colors.white : Colors.black87, + size: 24.sp, + ), ), - ), - SizedBox(width: 16.w), - // 设备信息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - device.devicename.isNotEmpty - ? device.devicename - : "deviceTypeUnknown".tr, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - SizedBox(height: 4.h), - Text( - device.devicemac.isNotEmpty - ? device.devicemac - : "noMacAddress".tr, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white70 : Colors.grey[600], + SizedBox(width: 16.w), + // 设备信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + device.devicename.isNotEmpty + ? device.devicename + : "deviceTypeUnknown".tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + ), ), - ), - if (device.license.isNotEmpty) ...[ SizedBox(height: 4.h), Text( - "${"licenseCode".tr}: ${device.license}", + device.devicemac.isNotEmpty + ? device.devicemac + : "noMacAddress".tr, style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white54 : Colors.grey[500], + fontSize: 14.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[600], ), ), + if (device.license.isNotEmpty) ...[ + SizedBox(height: 4.h), + Text( + "${"licenseCode".tr}: ${device.license}", + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white54 : Colors.grey[500], + ), + ), + ], ], - ], - ), - ), - // 解绑按钮 - GestureDetector( - onTap: () => _showUnbindDialog(device, isDarkMode), - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 12.w, - vertical: 6.h, ), - decoration: BoxDecoration( - color: Colors.red.withOpacity(0.1), - borderRadius: BorderRadius.circular(6.r), - border: Border.all( - color: Colors.red.withOpacity(0.3), - width: 1, + ), + // 解绑按钮 + GestureDetector( + onTap: () => _showUnbindDialog(device, isDarkMode), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 6.h, ), - ), - child: Text( - "unbind".tr, - style: TextStyle( - fontSize: 12.sp, - color: Colors.red, - fontWeight: FontWeight.w500, + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(6.r), + border: Border.all( + color: Colors.red.withOpacity(0.3), + width: 1, + ), + ), + child: Text( + "unbind".tr, + style: TextStyle( + fontSize: 12.sp, + color: Colors.red, + fontWeight: FontWeight.w500, + ), ), ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/modules/eq_settings/bindings/eq_settings_binding.dart b/lib/modules/eq_settings/bindings/eq_settings_binding.dart new file mode 100644 index 000000000..1f2884bd2 --- /dev/null +++ b/lib/modules/eq_settings/bindings/eq_settings_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import '../controllers/eq_settings_controller.dart'; + +class EqSettingsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => EqSettingsController()); + } +} diff --git a/lib/modules/eq_settings/controllers/eq_settings_controller.dart b/lib/modules/eq_settings/controllers/eq_settings_controller.dart new file mode 100644 index 000000000..12a373c0c --- /dev/null +++ b/lib/modules/eq_settings/controllers/eq_settings_controller.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +import '../../../core/utils/logger.dart'; +import '../../../routes/app_routes.dart'; +import '../views/widgets/row_confirm_widget.dart'; +import '../views/widgets/row_equalizer_widget.dart'; +import '../views/widgets/row_grade_widget.dart'; +import '../views/widgets/row_input_widget.dart'; +import '../views/widgets/row_keypress_widget.dart'; +import '../views/widgets/row_radio_widget.dart'; +import '../views/widgets/row_switch_widget.dart'; +import '../views/widgets/row_timer_widget.dart'; + +class EqSettingsController extends GetxController { + final RxList uiList = [ + { + 'title': '音效', + 'templates': [ + { + 'type': 'equalizer', + 'name': '均衡器', + 'value': [], + 'options': [ + { + 'label': '默认', + 'value': [1, 2, 3, 4, 5, 4, 3, 2, 1, 0], + 'default': true, + }, + { + 'label': '流行', + 'value': [1, 0, 3, 4, 5, 4, 1, 2, 1, 0], + 'default': true, + }, + { + 'label': '摇滚', + 'value': [2, 3, 4, 5, 4, 3, 2, 1, 0, -1], + 'default': true, + }, + { + 'label': '爵士', + 'value': [3, 4, 0, 4, 3, 2, 1, 0, -1, -2], + 'default': true, + }, + { + 'label': '经典', + 'value': [3, 4, 2, 4, 3, 2, 1, 0, -1, -2], + 'default': true, + }, + { + 'label': '乡村', + 'value': [3, 4, 1, 4, 3, 2, 1, 0, -1, -2], + 'default': true, + }, + ], + 'xAxis': [ + '8', + '31', + '62', + '125', + '250', + '500', + '1K', + '2K', + '4K', + '16K' + ], + 'onCallback': (List value) { + Logger.d('equalizer', '均衡器:$value'); + }, + } + ], + }, + { + 'title': '按键功能', + 'templates': [ + { + 'type': 'keypress', + 'data': [ + {'key': 'key1', 'label': '单击', 'left': 1, 'right': 1}, + {'key': 'key2', 'label': '双击', 'left': 1, 'right': 1}, + {'key': 'key3', 'label': '三击', 'left': 1, 'right': 1}, + {'key': 'key4', 'label': '长按', 'left': 1, 'right': 1}, + ], + 'options': [ + {'label': '无作用', 'value': 0}, + {'label': '回拨电话', 'value': 1}, + {'label': '语音助手', 'value': 2}, + {'label': '上一曲', 'value': 3}, + {'label': '下一曲', 'value': 4}, + {'label': '音量加', 'value': 5}, + {'label': '音量减', 'value': 6}, + {'label': '播放暂停', 'value': 7}, + {'label': '游戏模式', 'value': 8}, + {'label': '降噪设置', 'value': 9}, + {'label': 'EQ切换', 'value': 10}, + ], + 'onCallback': (Map value) { + Logger.d('keypress', '按键功能:$value'); + }, + }, + ], + }, + { + 'title': '耳机功能', + 'templates': [ + { + 'type': 'input', + 'name': '修改名称', + 'value': 'voitrans', + 'onCallback': (String value) { + Logger.d('input', '修改名称:$value'); + }, + }, + { + 'type': 'radio', + 'name': '工作模式', + 'value': 1, + 'options': [ + {'label': '游戏模式', 'value': 1}, + {'label': '娱乐模式', 'value': 2}, + {'label': '睡眠模式', 'value': 3}, + ], + 'onCallback': (int value) { + Logger.d('radio', '工作模式:$value'); + }, + }, + { + 'type': 'switch', + 'name': '闪灯开关', + 'value': false, + 'onCallback': (bool value) { + Logger.d('switch', '闪灯开关:$value'); + }, + }, + { + 'type': 'switch', + 'name': '查找设备', + 'value': false, + 'description': '在设备上播放声音', + 'onCallback': (bool value) { + Logger.d('switch', '查找设备:$value'); + }, + }, + { + 'type': 'grade', + 'name': '降噪等级', + 'value': 0, + 'onCallback': (int value) { + Logger.d('grade', '降噪等级:$value'); + }, + }, + { + 'type': 'timer', + 'name': '定时关机', + 'value': 0, + 'onCallback': (int value) { + Logger.d('timer', '定时关机:$value'); + }, + }, + ], + }, + { + 'templates': [ + { + 'type': 'confirm', + 'name': '升级耳机固件', + 'onCallback': () => Get.toNamed(Routes.OTA_UPGRADE), + }, + { + 'type': 'confirm', + 'name': '恢复出厂设置', + 'tips': '确定要恢复出厂设置吗?', + 'onCallback': () { + Logger.d('confirm', '恢复出厂设置'); + }, + }, + { + 'type': 'confirm', + 'name': '清除配对记录', + 'tips': '确定要清除配对记录吗?', + 'onCallback': () { + Logger.d('confirm', '清除配对记录'); + }, + }, + ], + } + ].obs; + + final GetStorage _storage = GetStorage(); + + @override + void onInit() { + super.onInit(); + final List equalizerOptions = _storage.read('equalizer_options') ?? []; + if (equalizerOptions.isNotEmpty) { + List options = + equalizerOptions.map((e) => Map.from(e)).toList(); + uiList[0]['templates'][0]['options'].addAll(options); + } + } + + Widget getTemplate(Map data) { + return switch (data['type']) { + 'equalizer' => RowEqualizerWidget(data), + 'keypress' => RowKeypressWidget(data), + 'input' => RowInputWidget(data), + 'radio' => RowRadioWidget(data), + 'switch' => RowSwitchWidget(data), + 'grade' => RowGradeWidget(data), + 'timer' => RowTimerWidget(data), + 'confirm' => RowConfirmWidget(data), + _ => const SizedBox.shrink(), + }; + } +} diff --git a/lib/modules/eq_settings/views/eq_settings_view.dart b/lib/modules/eq_settings/views/eq_settings_view.dart new file mode 100644 index 000000000..67f0403e9 --- /dev/null +++ b/lib/modules/eq_settings/views/eq_settings_view.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +import '../controllers/eq_settings_controller.dart'; + +class EqSettingsView extends GetView { + const EqSettingsView({super.key}); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: _buildBody(isDarkMode), + ); + } + + // 构建应用栏 + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + surfaceTintColor: Colors.transparent, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), + ), + title: Text( + '设备设置', + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + ); + } + + // 构建主体内容 + Widget _buildBody(bool isDarkMode) { + return Obx( + () => ListView.builder( + itemCount: controller.uiList.length, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), + itemBuilder: (BuildContext context, int index) { + Map data = controller.uiList[index]; + return _item(isDarkMode, data); + }, + ), + ); + } + + Widget _item(bool isDarkMode, Map data) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), + margin: EdgeInsets.symmetric(vertical: 5.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withValues(alpha: 0.05) : Colors.white, + borderRadius: BorderRadius.circular(8.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (data['title'] != null) + Text( + data['title'], + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ...data['templates'].map((e) => controller.getTemplate(e)).toList(), + ], + ), + ); + } +} diff --git a/lib/modules/eq_settings/views/widgets/row_confirm_widget.dart b/lib/modules/eq_settings/views/widgets/row_confirm_widget.dart new file mode 100644 index 000000000..ce680b1d0 --- /dev/null +++ b/lib/modules/eq_settings/views/widgets/row_confirm_widget.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +class RowConfirmWidget extends StatefulWidget { + final Map templateData; + + const RowConfirmWidget(this.templateData, {super.key}); + + @override + State createState() => _RowConfirmWidgetState(); +} + +class _RowConfirmWidgetState extends State { + Map get _templateData => widget.templateData; + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return GestureDetector( + onTap: () { + if (_templateData['tips'] != null) { + _alertDialog(isDarkMode); + } else { + _templateData['onCallback'](); + } + }, + behavior: HitTestBehavior.translucent, + child: Padding( + padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), + child: Row( + children: [ + Expanded( + child: Text( + _templateData['name'], + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + Icon( + Icons.chevron_right, + size: 26.w, + color: Colors.blue, + ), + ], + ), + ), + ); + } + + void _alertDialog(bool isDarkMode) { + Get.dialog( + AlertDialog( + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + title: Text( + _templateData['name'], + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + content: Text( + _templateData['tips'], + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + 'cancel'.tr, // 取消 + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ), + TextButton( + onPressed: () { + _templateData['onCallback'](); + Get.back(); + }, + child: Text( + 'confirm'.tr, // 确定 + style: TextStyle( + color: isDarkMode ? Colors.red[300] : Colors.red[600], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/eq_settings/views/widgets/row_equalizer_widget.dart b/lib/modules/eq_settings/views/widgets/row_equalizer_widget.dart new file mode 100644 index 000000000..cccd24734 --- /dev/null +++ b/lib/modules/eq_settings/views/widgets/row_equalizer_widget.dart @@ -0,0 +1,83 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +import 'select_equalizer_widget.dart'; + +class RowEqualizerWidget extends StatefulWidget { + final Map templateData; + + const RowEqualizerWidget(this.templateData, {super.key}); + + @override + State createState() => _RowEqualizerWidgetState(); +} + +class _RowEqualizerWidgetState extends State { + Map get _templateData => widget.templateData; + + String _label = ''; + + @override + void initState() { + super.initState(); + _label = _templateData['options'].firstWhere( + (option) => listEquals(_templateData['value'], option['value']), + orElse: () => {'label': ''}, + )['label']; + } + + void _setLabel() { + setState(() { + _label = _templateData['options'].firstWhere( + (option) => listEquals(_templateData['value'], option['value']), + orElse: () => {'label': ''}, + )['label']; + }); + } + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return GestureDetector( + onTap: () async { + var result = await Get.to(() => SelectEqualizerWidget(_templateData)); + _setLabel(); + if (result != null) { + _templateData['onCallback'](_templateData['value']); + } + }, + behavior: HitTestBehavior.translucent, + child: Padding( + padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), + child: Row( + children: [ + Expanded( + child: Text( + _templateData['name'], + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + Text( + _label, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white : Colors.grey, + ), + ), + Icon( + Icons.chevron_right, + size: 26.w, + color: Colors.blue, + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/eq_settings/views/widgets/row_grade_widget.dart b/lib/modules/eq_settings/views/widgets/row_grade_widget.dart new file mode 100644 index 000000000..8abd2d327 --- /dev/null +++ b/lib/modules/eq_settings/views/widgets/row_grade_widget.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class RowGradeWidget extends StatefulWidget { + final Map templateData; + + const RowGradeWidget(this.templateData, {super.key}); + + @override + State createState() => _RowGradeWidgetState(); +} + +class _RowGradeWidgetState extends State { + Map get _templateData => widget.templateData; + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Padding( + padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), + child: Row( + children: [ + Expanded( + child: Text( + _templateData['name'], + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + Slider( + value: _templateData['value'].toDouble(), + min: 0, + max: 10, + divisions: 10, + activeColor: Colors.blue, + onChanged: (value) { + setState(() { + _templateData['value'] = value.toInt(); + }); + }, + onChangeEnd: (value) { + _templateData['onCallback'](_templateData['value']); + }, + ), + SizedBox( + width: 15.w, + child: Text( + _templateData['value'].toString(), + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white : Colors.grey, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/eq_settings/views/widgets/row_input_widget.dart b/lib/modules/eq_settings/views/widgets/row_input_widget.dart new file mode 100644 index 000000000..339b79ce8 --- /dev/null +++ b/lib/modules/eq_settings/views/widgets/row_input_widget.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +import 'settings_input_widget.dart'; + +class RowInputWidget extends StatefulWidget { + final Map templateData; + + const RowInputWidget(this.templateData, {super.key}); + + @override + State createState() => _RowInputWidgetState(); +} + +class _RowInputWidgetState extends State { + Map get _templateData => widget.templateData; + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return GestureDetector( + onTap: () => _inputBottomSheet(isDarkMode), + behavior: HitTestBehavior.translucent, + child: Padding( + padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), + child: Row( + children: [ + Expanded( + child: Text( + _templateData['name'], + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + Text( + _templateData['value'], + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white : Colors.grey, + ), + ), + Icon( + Icons.chevron_right, + size: 26.w, + color: Colors.blue, + ), + ], + ), + ), + ); + } + + void _inputBottomSheet(bool isDarkMode) { + Get.bottomSheet( + SettingsInputWidget(_templateData['name'], _templateData['value']), + isScrollControlled: true, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ).then((value) { + if (value != null) { + setState(() { + _templateData['value'] = value; + }); + _templateData['onCallback'](value); + } + }); + } +} diff --git a/lib/modules/eq_settings/views/widgets/row_keypress_widget.dart b/lib/modules/eq_settings/views/widgets/row_keypress_widget.dart new file mode 100644 index 000000000..2e7722017 --- /dev/null +++ b/lib/modules/eq_settings/views/widgets/row_keypress_widget.dart @@ -0,0 +1,242 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +class RowKeypressWidget extends StatefulWidget { + final Map templateData; + + const RowKeypressWidget(this.templateData, {super.key}); + + @override + State createState() => _RowKeypressWidgetState(); +} + +class _RowKeypressWidgetState extends State { + Map get _templateData => widget.templateData; + + String _getLabel(int value) { + return _templateData['options'].firstWhere((item) => item['value'] == value, + orElse: () => {'label': ''})['label']; + } + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Row( + children: [ + const Spacer(), + Expanded( + child: Text( + '左', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + Expanded( + child: Text( + '右', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + ], + ), + ), + ..._templateData['data'] + .map((item) => _rowItem(isDarkMode, item)) + .toList(), + ], + ); + } + + Widget _rowItem(bool isDarkMode, Map item) { + return Row( + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Text( + item['label'], + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () => _radioDialog(isDarkMode, item, 'left'), + behavior: HitTestBehavior.translucent, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Text( + _getLabel(item['left']), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white : Colors.grey, + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () => _radioDialog(isDarkMode, item, 'right'), + behavior: HitTestBehavior.translucent, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Text( + _getLabel(item['right']), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white : Colors.grey, + ), + ), + ), + ), + ), + ], + ); + } + + void _radioDialog(bool isDarkMode, Map item, String key) { + Get.dialog( + Dialog( + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + child: Padding( + padding: EdgeInsets.all(12.w), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${item['label']}-${key == 'left' ? '左' : '右'}', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + SizedBox(height: 16.h), + Container( + constraints: BoxConstraints(maxHeight: 0.5.sh), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: _templateData['options'] + .map((option) => + _radioItem(isDarkMode, option, item, key)) + .toList(), + ), + ), + ), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + 'cancel'.tr, // 取消 + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _radioItem(bool isDarkMode, Map option, Map item, String key) { + return GestureDetector( + onTap: () { + setState(() { + item[key] = option['value']; + }); + Map data = { + 'key': item['key'], + key: option['value'], + }; + _templateData['onCallback'](data); + Get.back(); + }, + child: Container( + margin: EdgeInsets.only(bottom: 10.h), + padding: EdgeInsets.all(10.w), + decoration: BoxDecoration( + color: item[key] == option['value'] + ? (isDarkMode + ? Colors.teal[900]!.withValues(alpha: 0.3) + : Colors.teal[50]!) + : (isDarkMode + ? Colors.white.withValues(alpha: 0.05) + : Colors.grey[100]!), + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: item[key] == option['value'] + ? (isDarkMode ? Colors.teal[300]! : Colors.teal[600]!) + : Colors.transparent, + width: 1, + ), + ), + child: Row( + children: [ + Expanded( + child: Text( + option['label'], + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Container( + width: 24.w, + height: 24.w, + decoration: BoxDecoration( + color: item[key] == option['value'] + ? (isDarkMode ? Colors.teal[300] : Colors.teal[600]) + : (isDarkMode + ? Colors.white.withValues(alpha: 0.1) + : Colors.grey[300]), + shape: BoxShape.circle, + ), + child: item[key] == option['value'] + ? Icon( + Icons.check, + size: 16.w, + color: Colors.white, + ) + : null, + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/eq_settings/views/widgets/row_radio_widget.dart b/lib/modules/eq_settings/views/widgets/row_radio_widget.dart new file mode 100644 index 000000000..6dbfd46f1 --- /dev/null +++ b/lib/modules/eq_settings/views/widgets/row_radio_widget.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +class RowRadioWidget extends StatefulWidget { + final Map templateData; + + const RowRadioWidget(this.templateData, {super.key}); + + @override + State createState() => _RowRadioWidgetState(); +} + +class _RowRadioWidgetState extends State { + Map get _templateData => widget.templateData; + + String _label = ''; + + @override + void initState() { + super.initState(); + _label = _templateData['options'].firstWhere( + (option) => option['value'] == _templateData['value'], + orElse: () => {'label': ''}, + )['label']; + } + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return GestureDetector( + onTap: () => _radioDialog(isDarkMode), + behavior: HitTestBehavior.translucent, + child: Padding( + padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), + child: Row( + children: [ + Expanded( + child: Text( + _templateData['name'], + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + Text( + _label, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white : Colors.grey, + ), + ), + Icon( + Icons.chevron_right, + size: 26.w, + color: Colors.blue, + ), + ], + ), + ), + ); + } + + void _radioDialog(bool isDarkMode) { + Get.dialog( + Dialog( + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + child: Padding( + padding: EdgeInsets.all(12.w), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _templateData['name'], + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + SizedBox(height: 16.h), + Container( + constraints: BoxConstraints(maxHeight: 0.5.sh), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: _templateData['options'] + .map((option) => _item(isDarkMode, option)) + .toList(), + ), + ), + ), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + 'cancel'.tr, // 取消 + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _item(bool isDarkMode, Map option) { + return GestureDetector( + onTap: () { + setState(() { + _label = option['label']; + }); + _templateData['value'] = option['value']; + _templateData['onCallback'](option['value']); + Get.back(); + }, + child: Container( + margin: EdgeInsets.only(bottom: 10.h), + padding: EdgeInsets.all(10.w), + decoration: BoxDecoration( + color: _templateData['value'] == option['value'] + ? (isDarkMode + ? Colors.teal[900]!.withValues(alpha: 0.3) + : Colors.teal[50]!) + : (isDarkMode + ? Colors.white.withValues(alpha: 0.05) + : Colors.grey[100]!), + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: _templateData['value'] == option['value'] + ? (isDarkMode ? Colors.teal[300]! : Colors.teal[600]!) + : Colors.transparent, + width: 1, + ), + ), + child: Row( + children: [ + Expanded( + child: Text( + option['label'], + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Container( + width: 24.w, + height: 24.w, + decoration: BoxDecoration( + color: _templateData['value'] == option['value'] + ? (isDarkMode ? Colors.teal[300] : Colors.teal[600]) + : (isDarkMode + ? Colors.white.withValues(alpha: 0.1) + : Colors.grey[300]), + shape: BoxShape.circle, + ), + child: _templateData['value'] == option['value'] + ? Icon( + Icons.check, + size: 16.w, + color: Colors.white, + ) + : null, + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/eq_settings/views/widgets/row_switch_widget.dart b/lib/modules/eq_settings/views/widgets/row_switch_widget.dart new file mode 100644 index 000000000..e21223ba5 --- /dev/null +++ b/lib/modules/eq_settings/views/widgets/row_switch_widget.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class RowSwitchWidget extends StatefulWidget { + final Map templateData; + + const RowSwitchWidget(this.templateData, {super.key}); + + @override + State createState() => _RowSwitchWidgetState(); +} + +class _RowSwitchWidgetState extends State { + Map get _templateData => widget.templateData; + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Padding( + padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), + child: Row( + children: [ + Expanded( + child: Text( + _templateData['name'], + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + if (_templateData['description'] != null) + Text( + _templateData['description'], + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white : Colors.grey, + ), + ), + SizedBox( + height: 26.w, + child: Transform.scale( + scale: 0.8, + alignment: Alignment.centerRight, + child: Switch( + value: _templateData['value'], + onChanged: (bool value) { + setState(() { + _templateData['value'] = value; + }); + _templateData['onCallback'](value); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/eq_settings/views/widgets/row_timer_widget.dart b/lib/modules/eq_settings/views/widgets/row_timer_widget.dart new file mode 100644 index 000000000..2a950f60e --- /dev/null +++ b/lib/modules/eq_settings/views/widgets/row_timer_widget.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +import 'select_timer_widget.dart'; + +class RowTimerWidget extends StatefulWidget { + final Map templateData; + + const RowTimerWidget(this.templateData, {super.key}); + + @override + State createState() => _RowTimerWidgetState(); +} + +class _RowTimerWidgetState extends State { + Map get _templateData => widget.templateData; + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return GestureDetector( + onTap: () => _timerBottomSheet(isDarkMode), + behavior: HitTestBehavior.translucent, + child: Padding( + padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), + child: Row( + children: [ + Expanded( + child: Text( + _templateData['name'], + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + if (_templateData['value'] > 0) + Text( + '${_templateData['value']}分钟', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white : Colors.grey, + ), + ), + Icon( + Icons.chevron_right, + size: 26.w, + color: Colors.blue, + ), + ], + ), + ), + ); + } + + void _timerBottomSheet(bool isDarkMode) { + Get.bottomSheet( + SelectTimerWidget(_templateData['name'], _templateData['value']), + isScrollControlled: true, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ).then((value) { + if (value != null) { + setState(() { + _templateData['value'] = value; + }); + _templateData['onCallback'](value); + } + }); + } +} diff --git a/lib/modules/eq_settings/views/widgets/select_equalizer_widget.dart b/lib/modules/eq_settings/views/widgets/select_equalizer_widget.dart new file mode 100644 index 000000000..ea04be1f5 --- /dev/null +++ b/lib/modules/eq_settings/views/widgets/select_equalizer_widget.dart @@ -0,0 +1,305 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +import 'settings_input_widget.dart'; + +class SelectEqualizerWidget extends StatefulWidget { + final Map templateData; + + const SelectEqualizerWidget(this.templateData, {super.key}); + + @override + State createState() => _SelectEqualizerWidgetState(); +} + +class _SelectEqualizerWidgetState extends State { + Map get _templateData => widget.templateData; + + List _values = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + bool isEdit = false; + + final GetStorage _storage = GetStorage(); + List _equalizerOptions = []; + + @override + void initState() { + super.initState(); + if (_templateData['value'].isNotEmpty) { + _values = List.from(_templateData['value']); + } + _equalizerOptions = _storage.read('equalizer_options') ?? []; + } + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: _buildBody(isDarkMode), + ); + } + + // 构建应用栏 + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + surfaceTintColor: Colors.transparent, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), + ), + title: Text( + _templateData['name'], + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + actions: [ + GestureDetector( + onTap: () { + setState(() { + isEdit = !isEdit; + }); + }, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 6.w, + ), + child: Text( + isEdit ? 'cancel'.tr : 'edit'.tr, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 16.sp, + ), + ), + ), + ), + ], + ); + } + + // 构建主体内容 + Widget _buildBody(bool isDarkMode) { + bool isAdd = true; + + return ListView( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), + children: [ + _equalizer(isDarkMode), + Wrap( + spacing: 10.w, + runSpacing: 10.w, + children: [ + ..._templateData['options'].map((Map option) { + bool isSelected = listEquals(_values, option['value']); + if (isSelected) { + isAdd = false; + } + return _item(isDarkMode, option, isSelected); + }).toList(), + if (isAdd) + GestureDetector( + onTap: () => _inputBottomSheet(isDarkMode), + child: Container( + width: 110.w, + height: 110.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withValues(alpha: 0.05) + : Colors.white, + borderRadius: BorderRadius.circular(8.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Icon( + Icons.add, + size: 40.sp, + color: Colors.green, + ), + ), + ), + ], + ), + ], + ); + } + + Widget _equalizer(bool isDarkMode) { + return Container( + height: 220.w, + margin: EdgeInsets.only(bottom: 5.w), + child: Row( + children: List.generate( + _templateData['xAxis'].length, + (index) => Expanded( + child: Stack( + children: [ + RotatedBox( + quarterTurns: 3, + child: StatefulBuilder( + builder: (BuildContext context, setStateful) { + return Slider( + value: _values[index].toDouble(), + min: -5, + max: 5, + divisions: 10, + activeColor: Colors.green, + onChanged: (value) { + setStateful(() { + _values[index] = value.toInt(); + }); + }, + onChangeEnd: (value) { + setState(() {}); + }, + ); + }, + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Text( + _templateData['xAxis'][index], + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.grey, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _item(bool isDarkMode, Map option, bool isSelected) { + return GestureDetector( + onTap: () { + _templateData['value'] = option['value']; + Get.back(result: true); + }, + child: Stack( + children: [ + Container( + width: 110.w, + height: 110.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: isSelected + ? Colors.green + : isDarkMode + ? Colors.white.withValues(alpha: 0.05) + : Colors.white, + borderRadius: BorderRadius.circular(8.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Text( + option['label'], + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isSelected + ? Colors.white + : isDarkMode + ? Colors.white + : Colors.black, + ), + ), + ), + if (isEdit && !option['default']) + Positioned( + top: 4.w, + right: 4.w, + child: GestureDetector( + onTap: () { + if (isSelected) { + _templateData['value'] = []; + _templateData['onCallback']([]); + } + _equalizerOptions.removeWhere( + (e) => listEquals(e['value'], option['value'])); + _storage.write('equalizer_options', _equalizerOptions); + setState(() { + _templateData['options'].remove(option); + }); + }, + child: Container( + width: 24.w, + height: 24.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(12.w), + ), + child: Icon( + Icons.close, + size: 16.sp, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ); + } + + void _inputBottomSheet(bool isDarkMode) { + Get.bottomSheet( + const SettingsInputWidget('添加均衡器', ''), + isScrollControlled: true, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ).then((value) { + if (value != null) { + Map option = { + 'label': value, + 'value': List.from(_values), + 'default': false, + }; + _equalizerOptions.add(option); + _storage.write('equalizer_options', _equalizerOptions); + setState(() { + _templateData['options'].add(option); + }); + } + }); + } +} diff --git a/lib/modules/eq_settings/views/widgets/select_timer_widget.dart b/lib/modules/eq_settings/views/widgets/select_timer_widget.dart new file mode 100644 index 000000000..ec082d4e2 --- /dev/null +++ b/lib/modules/eq_settings/views/widgets/select_timer_widget.dart @@ -0,0 +1,231 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +class SelectTimerWidget extends StatefulWidget { + final String title; + final int time; + + const SelectTimerWidget(this.title, this.time, {super.key}); + + @override + State createState() => _SelectTimerWidgetState(); +} + +class _SelectTimerWidgetState extends State { + late FixedExtentScrollController _hourScrollController; + late FixedExtentScrollController _minuteScrollController; + + int _hour = 0; + int _minute = 0; + + @override + void initState() { + super.initState(); + _hour = widget.time ~/ 60; + _minute = widget.time % 60; + _hourScrollController = FixedExtentScrollController(initialItem: _hour); + _minuteScrollController = FixedExtentScrollController(initialItem: _minute); + } + + @override + void dispose() { + super.dispose(); + _hourScrollController.dispose(); + _minuteScrollController.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.title, + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + fontWeight: FontWeight.bold, + ), + ), + GestureDetector( + onTap: () => Get.back(), + child: Container( + width: 26.w, + height: 26.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(26.r), + ), + child: Icon( + Icons.close, + size: 18.w, + color: Colors.grey[400], + ), + ), + ) + ], + ), + ), + Divider( + height: 1, + color: Colors.grey[300], + ), + SizedBox( + height: 220.w, + child: Row( + children: [ + Expanded( + child: CupertinoPicker.builder( + itemExtent: 40.w, + diameterRatio: 1.1, + squeeze: 1.25, + childCount: 24, + selectionOverlay: _selectionOverlayWidget( + isRadiusLeft: true, + ), + scrollController: _hourScrollController, + itemBuilder: (BuildContext context, int index) { + return Container( + padding: EdgeInsets.only(right: 10.w), + alignment: Alignment.centerRight, + child: Text( + '$index时', + style: _hour == index + ? TextStyle( + color: const Color(0xFFFF5561), + fontSize: 15.sp, + fontWeight: FontWeight.w500, + ) + : TextStyle( + color: const Color(0xFF9D9D9D), + fontSize: 15.sp, + ), + ), + ); + }, + onSelectedItemChanged: (int index) { + setState(() { + _hour = index; + }); + }, + ), + ), + Expanded( + child: CupertinoPicker.builder( + itemExtent: 40.w, + diameterRatio: 1.1, + squeeze: 1.25, + childCount: 60, + selectionOverlay: _selectionOverlayWidget( + isRadiusRight: true, + ), + scrollController: _minuteScrollController, + itemBuilder: (BuildContext context, int index) { + return Container( + padding: EdgeInsets.only(left: 10.w), + alignment: Alignment.centerLeft, + child: Text( + '$index分', + style: _minute == index + ? TextStyle( + color: const Color(0xFFFF5561), + fontSize: 15.sp, + fontWeight: FontWeight.w500, + ) + : TextStyle( + color: const Color(0xFF9D9D9D), + fontSize: 15.sp, + ), + ), + ); + }, + onSelectedItemChanged: (int index) { + setState(() { + _minute = index; + }); + }, + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 20.w), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + 'cancel'.tr, // 取消 + style: TextStyle( + fontSize: 16.sp, + color: Colors.black, + ), + ), + ), + ), + ), + 10.horizontalSpace, + Expanded( + child: GestureDetector( + onTap: () { + int time = _hour * 60 + _minute; + Get.back(result: time); + }, + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + 'confirm'.tr, // 确认 + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _selectionOverlayWidget({ + bool isRadiusLeft = false, + bool isRadiusRight = false, + }) { + return Container( + decoration: BoxDecoration( + color: const Color.fromARGB(45, 255, 85, 97), + borderRadius: BorderRadius.horizontal( + left: isRadiusLeft ? Radius.circular(8.w) : Radius.zero, + right: isRadiusRight ? Radius.circular(8.w) : Radius.zero, + ), + ), + ); + } +} diff --git a/lib/modules/eq_settings/views/widgets/settings_input_widget.dart b/lib/modules/eq_settings/views/widgets/settings_input_widget.dart new file mode 100644 index 000000000..ea41dd49c --- /dev/null +++ b/lib/modules/eq_settings/views/widgets/settings_input_widget.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +class SettingsInputWidget extends StatefulWidget { + final String title; + final String inputText; + + const SettingsInputWidget(this.title, this.inputText, {super.key}); + + @override + State createState() => _SettingsInputWidgetState(); +} + +class _SettingsInputWidgetState extends State { + final TextEditingController _inputController = TextEditingController(); + + @override + void initState() { + super.initState(); + _inputController.text = widget.inputText; + } + + @override + void dispose() { + _inputController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.title, + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + fontWeight: FontWeight.bold, + ), + ), + GestureDetector( + onTap: () => Get.back(), + child: Container( + width: 26.w, + height: 26.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(26.r), + ), + child: Icon( + Icons.close, + size: 18.w, + color: Colors.grey[400], + ), + ), + ) + ], + ), + ), + Divider( + height: 1, + color: Colors.grey[300], + ), + Container( + margin: EdgeInsets.symmetric(vertical: 20.w), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: TextField( + controller: _inputController, + style: TextStyle( + color: Colors.black87, + fontSize: 16.sp, + ), + decoration: InputDecoration( + hintText: 'maxCharacters'.tr, + hintStyle: TextStyle( + color: Colors.black54, + fontSize: 16.sp, + ), + isDense: true, + fillColor: Colors.transparent, + border: InputBorder.none, + contentPadding: EdgeInsets.all(10.w), + ), + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 20.w), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + 'cancel'.tr, // 取消 + style: TextStyle( + fontSize: 16.sp, + color: Colors.black, + ), + ), + ), + ), + ), + 10.horizontalSpace, + Expanded( + child: GestureDetector( + onTap: () { + if (_inputController.text.isNotEmpty) { + Get.back(result: _inputController.text); + } + }, + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + 'confirm'.tr, // 确认 + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/explore/controllers/explore_controller.dart b/lib/modules/explore/controllers/explore_controller.dart index d5c3b811b..c01c78de4 100644 --- a/lib/modules/explore/controllers/explore_controller.dart +++ b/lib/modules/explore/controllers/explore_controller.dart @@ -27,30 +27,7 @@ class AIFeature { }); } -class FunctionScene { - final String title; - final String description; - final IconData icon; - final Color startColor; - final Color endColor; - final Color iconColor; - final String targetRoute; - - FunctionScene({ - required this.title, - required this.description, - required this.icon, - required this.startColor, - required this.endColor, - required this.iconColor, - required this.targetRoute, - }); -} - class ExploreController extends GetxController { - // 搜索关键词 - final searchController = TextEditingController(); - // AI功能列表 final List aiFeatures = [ AIFeature( @@ -75,42 +52,6 @@ class ExploreController extends GetxController { agentId: 'simultaneous_interpreter', targetRoute: Routes.translationMiddleware, ), - // AIFeature( - // title: 'conferenceAssistant'.tr, // 商务会议助手 - // description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译 - // icon: Icons.people, - // startColor: const Color(0xFFab47bc), - // endColor: const Color(0xFFec407a), - // iconBgColor: const Color(0xFFd81b60).withOpacity(0.2), - // iconColor: const Color(0xFFd81b60), - // agentId: 'meeting_assistant', - // targetRoute: Routes.meeting, - // ), - // AIFeature( - // title: 'aiGirlfriend'.tr, // 亲子陪伴 - // description: 'aiGirlfriendInfo'.tr, // 智能陪伴,随时聊天 - // icon: Icons.favorite, - // startColor: const Color(0xFFE91E63), - // endColor: const Color(0xFFFF5252), - // iconBgColor: const Color(0xFFE91E63).withOpacity(0.2), - // iconColor: const Color(0xFFE91E63), - // agentId: 'cyber_girlfriend', - // targetRoute: Routes.chat, - // ), - // AIFeature( - // title: 'healthConsultant'.tr, // 健康顾问 - // description: 'healthConsultantInfo'.tr, // 提供科学健康建议 - // icon: Icons.healing, - // startColor: const Color(0xFFFFC107), - // endColor: const Color(0xFFFF9800), - // iconBgColor: const Color(0xFFFFC107).withOpacity(0.2), - // iconColor: const Color(0xFFFFC107), - // agentId: 'health_advisor', - // targetRoute: Routes.chat, - // ), - ]; - - final List aiFeaturesWide = [ AIFeature( title: 'conferenceAssistant'.tr, // 商务会议助手 description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译 @@ -122,155 +63,27 @@ class ExploreController extends GetxController { agentId: 'meeting_assistant', targetRoute: Routes.meeting, ), + AIFeature( + title: 'usageStats'.tr, // 用量统计 + description: 'usageStatsInfo'.tr, // 查看您的使用统计数据 + icon: Icons.analytics_outlined, + startColor: const Color(0xFF2196F3), // 科技蓝 + endColor: const Color(0xFF00BCD4), // 青蓝色 + iconBgColor: const Color(0xFF2196F3).withOpacity(0.2), + iconColor: const Color(0xFF2196F3), + agentId: 'usage_stats', + targetRoute: Routes.usageStats, + ) ]; - // 热门功能场景列表 - final List functionScenes = [ - FunctionScene( - title: 'conferenceAssistant'.tr, // 商务会议助手 - description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译 - icon: Icons.people, - startColor: const Color(0xFFab47bc), - endColor: const Color(0xFFec407a), - iconColor: const Color(0xFFd81b60), - targetRoute: Routes.meeting, - ), - FunctionScene( - title: 'studyAssistant'.tr, // 语言学习助手 - description: 'studyAssistantInfo'.tr, // 发音纠正,实时练习口语 - icon: Icons.school, - startColor: const Color(0xFF4CAF50), - endColor: const Color(0xFF009688), - iconColor: const Color(0xFF4CAF50), - targetRoute: Routes.realtime, - ), - FunctionScene( - title: 'travelTranslator'.tr, // 旅行翻译助手 - description: 'travelTranslatorInfo'.tr, // 实时语音翻译,旅游景点介绍 - icon: Icons.language, - startColor: const Color(0xFFFFC107), - endColor: const Color(0xFFFF9800), - iconColor: const Color(0xFFFFC107), - targetRoute: '', - ), - ]; - - // 搜索功能 - void search() { - if (searchController.text.isEmpty) return; - - Get.snackbar( - 'search'.tr, // 搜索 - '${'beSearching'.tr} ${searchController.text}', // 正在搜索: - snackPosition: SnackPosition.TOP, - ); - - // 实际应用中,这里应该实现真正的搜索逻辑 - searchController.clear(); - } - // 点击AI功能 void onAIFeatureTap(int index) { if (index < 0 || index >= aiFeatures.length) return; final feature = aiFeatures[index]; - final agent = AgentProvider.getAgentById(feature.agentId); - - if (agent == null) { - Get.snackbar( - 'error'.tr, // 错误 - 'noAssistantFound'.tr, // 未找到对应的AI助手 - snackPosition: SnackPosition.TOP, - ); - return; - } + if (feature.targetRoute.isEmpty) return; // 根据目标路由进行跳转 - if (feature.targetRoute == Routes.chat) { - // 跳转到聊天页面 - Get.toNamed(feature.targetRoute, arguments: { - 'agentId': agent.id, - 'agentName': agent.name, - 'agentVoice': agent.voice, - 'agentAvatar': agent.avatarUrl, - 'agentSubtitle': agent.description, - 'systemPrompt': agent.systemPrompt, - 'welcomeMessage': agent.welcomeMessage, - }); - } else if (feature.targetRoute == Routes.translationMiddleware) { - // 跳转到翻译页面 - Get.toNamed(feature.targetRoute); - } else if (feature.targetRoute == Routes.agent) { - Get.toNamed(feature.targetRoute); - } else if (feature.targetRoute == Routes.meeting) { - // 跳转到会议页面 - Get.toNamed(feature.targetRoute); - } - } - - // 点击AI功能(宽版本) - void onAIFeatureWideTap(int index) { - if (index < 0 || index >= aiFeaturesWide.length) return; - - final feature = aiFeaturesWide[index]; - - // 直接根据目标路由进行跳转 - if (feature.targetRoute == Routes.meeting) { - Get.toNamed(feature.targetRoute); - } else if (feature.targetRoute == Routes.chat) { - final agent = AgentProvider.getAgentById(feature.agentId); - if (agent != null) { - Get.toNamed(feature.targetRoute, arguments: { - 'agentId': agent.id, - 'agentName': agent.name, - 'agentVoice': agent.voice, - 'agentAvatar': agent.avatarUrl, - 'agentSubtitle': agent.description, - 'systemPrompt': agent.systemPrompt, - 'welcomeMessage': agent.welcomeMessage, - }); - } - } else if (feature.targetRoute.isNotEmpty) { - Get.toNamed(feature.targetRoute); - } - } - - // 点击功能场景 - void onFunctionSceneTap(int index) { - if (index < 0 || index >= functionScenes.length) return; - - final scene = functionScenes[index]; - - if (scene.targetRoute.isNotEmpty) { - Get.toNamed(scene.targetRoute); - } else { - Get.snackbar( - 'scene'.tr, // 场景 - '${'youHaveSelected'.tr} ${functionScenes[index].title}', // 您选择了: - snackPosition: SnackPosition.TOP, - ); - } - } - - // 尝试新功能 - void tryNewFeature() { - Get.snackbar( - 'newFunction'.tr, // 新功能 - '${'youChoseTry'.tr}${'aiSinger'.tr}', // 您选择了尝试 // AI歌手模式 - snackPosition: SnackPosition.TOP, - ); - - // 实际应用中,这里应该跳转到新功能页面 - } - - // 跳转到翻译统计页面 - void goToTranslationStats() { - Get.toNamed(Routes.translationStats); - } - - @override - void onClose() { - searchController.dispose(); - super.onClose(); + Get.toNamed(feature.targetRoute); } } diff --git a/lib/modules/explore/views/explore_view.dart b/lib/modules/explore/views/explore_view.dart index 2c9c076ee..45e6c9cde 100644 --- a/lib/modules/explore/views/explore_view.dart +++ b/lib/modules/explore/views/explore_view.dart @@ -25,7 +25,7 @@ class ExploreView extends GetView { child: SingleChildScrollView( //physics: const BouncingScrollPhysics(), child: Padding( - padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 0.h), + padding: EdgeInsets.fromLTRB(16.w, 16.h, 16.w, 0.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -66,14 +66,6 @@ class ExploreView extends GetView { // AI功能列表 SizedBox(height: 24.h), _buildCustomAILayout(isDarkMode), - - // 热门功能场景 - SizedBox(height: 24.h), - //_buildFunctionScenesSection(isDarkMode), - - // 新功能推荐 - SizedBox(height: 24.h), - //_buildNewFeatureSection(isDarkMode), ], ), ), @@ -104,59 +96,27 @@ class ExploreView extends GetView { color: isDarkMode ? Colors.white : Colors.grey[800], ), ), - Container( - width: 32.w, - height: 32.w, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[200], - shape: BoxShape.circle, - ), - child: IconButton( - icon: Icon( - Icons.analytics_outlined, - size: 16.sp, - color: isDarkMode ? Colors.white : Colors.grey[700], - ), - padding: EdgeInsets.zero, - onPressed: controller.goToTranslationStats, - ), - ), + // Container( + // width: 32.w, + // height: 32.w, + // decoration: BoxDecoration( + // color: isDarkMode + // ? Colors.white.withOpacity(0.1) + // : Colors.grey[200], + // shape: BoxShape.circle, + // ), + // child: IconButton( + // icon: Icon( + // Icons.analytics_outlined, + // size: 16.sp, + // color: isDarkMode ? Colors.white : Colors.grey[700], + // ), + // padding: EdgeInsets.zero, + // onPressed: controller.goToUsageStats, + // ), + // ), ], ), - - // // 搜索框 - // SizedBox(height: 16.h), - // Container( - // decoration: BoxDecoration( - // color: - // isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], - // borderRadius: BorderRadius.circular(12.r), - // ), - // child: TextField( - // controller: controller.searchController, - // decoration: InputDecoration( - // hintText: 'searchAi'.tr, // 搜索AI功能... - // hintStyle: TextStyle( - // fontSize: 14.sp, - // color: isDarkMode ? Colors.grey[400] : Colors.grey[400], - // ), - // prefixIcon: Icon( - // Icons.search, - // size: 20.sp, - // color: isDarkMode ? Colors.grey[400] : Colors.grey[400], - // ), - // border: InputBorder.none, - // contentPadding: EdgeInsets.symmetric(vertical: 12.h), - // ), - // textInputAction: TextInputAction.search, - // onSubmitted: (_) => controller.search(), - // style: TextStyle( - // color: isDarkMode ? Colors.white : Colors.black, - // ), - // ), - // ), ], ), ); @@ -250,23 +210,8 @@ class ExploreView extends GetView { ); } - // 自定义AI功能布局:2x2网格,商务会议助手占据下方两个网格位置 + // 自定义AI功能布局:2x2网格 Widget _buildCustomAILayout(bool isDarkMode) { - // 找到AI全能助手 - final aiAssistantIndex = controller.aiFeatures.indexWhere( - (feature) => feature.agentId == 'personal_assistant', - ); - - // 找到同声传译 - final translationIndex = controller.aiFeatures.indexWhere( - (feature) => feature.agentId == 'simultaneous_interpreter', - ); - - // 找到商务会议助手(在aiFeaturesWide中) - final meetingFeature = controller.aiFeaturesWide.isNotEmpty - ? controller.aiFeaturesWide.first - : null; - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -287,47 +232,44 @@ class ExploreView extends GetView { Row( children: [ // 左上角:AI全能助手 - if (aiAssistantIndex != -1) - Expanded( - child: AspectRatio( - aspectRatio: 1.0, // 正方形 - child: _buildAIFeatureItem( - controller.aiFeatures[aiAssistantIndex], - aiAssistantIndex, - isDarkMode, - ), + Expanded( + child: AspectRatio( + aspectRatio: 1.0, // 正方形 + child: _buildAIFeatureItem( + controller.aiFeatures[0], + 0, + isDarkMode, ), ), + ), SizedBox(width: 16.w), // 右上角:同声传译 - if (translationIndex != -1) - Expanded( - child: AspectRatio( - aspectRatio: 1.0, // 正方形 - child: _buildAIFeatureItem( - controller.aiFeatures[translationIndex], - translationIndex, - isDarkMode, - ), + Expanded( + child: AspectRatio( + aspectRatio: 1.0, // 正方形 + child: _buildAIFeatureItem( + controller.aiFeatures[1], + 1, + isDarkMode, ), ), + ), ], ), SizedBox(height: 16.h), - // 下层:商务会议助手(横跨整行) - if (meetingFeature != null) - AspectRatio( - aspectRatio: 2.0, // 宽高比2:1,横跨两个网格位置 - child: _buildAIFeatureItemWideForMeeting( - meetingFeature, - 0, // 使用固定索引,因为这是aiFeaturesWide中的项目 - isDarkMode, - ), + // 下层:商务会议助手(占据一行) + AspectRatio( + aspectRatio: 2.0, // 正方形 + child: _buildAIFeatureItemWide( + controller.aiFeatures[2], + 2, + isDarkMode, ), + ), ], ), ], @@ -417,123 +359,11 @@ class ExploreView extends GetView { ); } -//整行布局 - Widget _buildAIFeaturesWideSection(bool isDarkMode) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Text( - // 'aiFunction'.tr, // AI功能 - // style: TextStyle( - // fontSize: 18.sp, - // fontWeight: FontWeight.w600, - // color: isDarkMode ? Colors.white : Colors.grey[800], - // ), - // ), - // SizedBox(height: 16.h), - // 直接使用 Column 包裹宽版卡片 - Column( - children: controller.aiFeaturesWide.map((feature) { - return Padding( - padding: EdgeInsets.only(bottom: 16.h), // 卡片间距 - child: _buildAIFeatureItemWide( - feature, - controller.aiFeatures.indexOf(feature), - isDarkMode, - ), - ); - }).toList(), - ), - ], - ); - } - - // AI功能项(宽版本,用于横跨整行的AI全能助手) + // AI功能项(宽版本) Widget _buildAIFeatureItemWide( AIFeature feature, int index, bool isDarkMode) { return GestureDetector( onTap: () => controller.onAIFeatureTap(index), - child: Container( - padding: EdgeInsets.all(20.w), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - feature.startColor.withOpacity(isDarkMode ? 0.3 : 0.3), - feature.endColor.withOpacity(isDarkMode ? 0.3 : 0.3), - ], - ), - borderRadius: BorderRadius.circular(20.r), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.grey.withOpacity(0.1), - blurRadius: 8.w, - offset: const Offset(0, 4), - ), - ], - ), - child: Row( - children: [ - // 图标圆圈 - Container( - width: 48.w, - height: 48.w, - decoration: BoxDecoration( - color: feature.iconColor.withValues(alpha: 0.2), - shape: BoxShape.circle, - ), - child: Icon( - feature.icon, - size: 24.sp, - color: feature.iconColor, - ), - ), - SizedBox(width: 16.w), - // 文本内容 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 标题 - Text( - feature.title, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.grey[800], - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 4.h), - // 描述 - Text( - feature.description, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), - ), - ); - } - - // AI功能项(宽版本,专门用于商务会议助手) - Widget _buildAIFeatureItemWideForMeeting( - AIFeature feature, int index, bool isDarkMode) { - return GestureDetector( - onTap: () => controller.onAIFeatureWideTap(index), child: Container( padding: EdgeInsets.all(20.w), decoration: BoxDecoration( @@ -609,202 +439,4 @@ class ExploreView extends GetView { ), ); } - - // 热门功能场景部分 - Widget _buildFunctionScenesSection(bool isDarkMode) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Text( - // 'popularFunction'.tr, // 热门功能场景 - // style: TextStyle( - // fontSize: 18.sp, - // fontWeight: FontWeight.w600, - // color: isDarkMode ? Colors.white : Colors.grey[800], - // ), - // ), - // SizedBox(height: 16.h), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: controller.functionScenes.length, - itemBuilder: (context, index) { - final scene = controller.functionScenes[index]; - return Padding( - padding: EdgeInsets.only(bottom: 16.h), - child: _buildFunctionSceneItem(scene, index, isDarkMode), - ); - }, - ), - ], - ); - } - - // 功能场景项 - Widget _buildFunctionSceneItem( - FunctionScene scene, int index, bool isDarkMode) { - return GestureDetector( - onTap: () => controller.onFunctionSceneTap(index), - child: Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.05) - : Colors.grey[100], - borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withValues(alpha: 0.2) - : Colors.grey.withValues(alpha: 0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - children: [ - // 图标 - Container( - width: 48.w, - height: 48.w, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - scene.startColor.withOpacity(isDarkMode ? 0.3 : 0.3), - scene.endColor.withOpacity(isDarkMode ? 0.3 : 0.3), - ], - ), - borderRadius: BorderRadius.circular(12.r), - ), - child: Icon( - scene.icon, - size: 24.sp, - color: scene.iconColor, - ), - ), - SizedBox(width: 16.w), - - // 文本内容 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - scene.title, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.grey[800], - ), - ), - SizedBox(height: 4.h), - Text( - scene.description, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - ), - ], - ), - ), - ], - ), - ), - ); - } - - // 新功能推荐部分 - Widget _buildNewFeatureSection(bool isDarkMode) { - return Container( - padding: EdgeInsets.all(20.w), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - const Color(0xFF9C27B0).withOpacity(isDarkMode ? 0.3 : 0.3), - const Color(0xFFE91E63).withOpacity(isDarkMode ? 0.3 : 0.3), - ], - ), - borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.grey.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 图标 - Container( - width: 48.w, - height: 48.w, - decoration: BoxDecoration( - color: const Color(0xFF9C27B0).withOpacity(0.2), - borderRadius: BorderRadius.circular(12.r), - ), - child: Icon( - Icons.lightbulb_outline, - size: 24.sp, - color: const Color(0xFF9C27B0), - ), - ), - SizedBox(height: 16.h), - - // 标题 - Text( - 'aiSinger'.tr, // AI歌手模式 - style: TextStyle( - fontSize: 20.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.grey[800], - ), - ), - SizedBox(height: 8.h), - - // 描述 - Text( - 'aiSingerInfo'.tr, // 用你喜欢的歌手声音演唱任何歌曲,甚至是你自己创作的歌曲 - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.grey[300] : Colors.grey[600], - ), - ), - SizedBox(height: 16.h), - - // 按钮 - ElevatedButton( - onPressed: controller.tryNewFeature, - style: ElevatedButton.styleFrom( - backgroundColor: isDarkMode - ? const Color(0xFF9C27B0) - : Colors.white.withOpacity(0.2), - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.r), - ), - ), - child: Text( - 'tryNow'.tr, // 立即尝试 - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : Colors.grey[800], - ), - ), - ), - ], - ), - ); - } } diff --git a/lib/modules/goods/bindings/goodsai_binding.dart b/lib/modules/goods/bindings/goodsai_binding.dart new file mode 100644 index 000000000..4c21e4920 --- /dev/null +++ b/lib/modules/goods/bindings/goodsai_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import '../controllers/goodsai_controller.dart'; + +class GoodsaiBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => GoodsaiController()); + } +} \ No newline at end of file diff --git a/lib/modules/goods/bindings/goodsmeet_binding.dart b/lib/modules/goods/bindings/goodsmeet_binding.dart new file mode 100644 index 000000000..f8c1b3308 --- /dev/null +++ b/lib/modules/goods/bindings/goodsmeet_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import '../controllers/goodsmeet_controller.dart'; + +class GoodsmeetBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => GoodsmeetController()); + } +} \ No newline at end of file diff --git a/lib/modules/goods/bindings/goodstrans_binding.dart b/lib/modules/goods/bindings/goodstrans_binding.dart new file mode 100644 index 000000000..696eaeb7e --- /dev/null +++ b/lib/modules/goods/bindings/goodstrans_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import '../controllers/goodstrans_controller.dart'; + +class GoodstransBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => GoodstransController()); + } +} diff --git a/lib/modules/goods/bindings/goodsvip_binding.dart b/lib/modules/goods/bindings/goodsvip_binding.dart new file mode 100644 index 000000000..50e957d57 --- /dev/null +++ b/lib/modules/goods/bindings/goodsvip_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import '../controllers/goodsvip_controller.dart'; + +class GoodsvipBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => GoodsvipController()); + } +} \ No newline at end of file diff --git a/lib/modules/goods/controllers/goods_base_controller.dart b/lib/modules/goods/controllers/goods_base_controller.dart new file mode 100644 index 000000000..14a779abc --- /dev/null +++ b/lib/modules/goods/controllers/goods_base_controller.dart @@ -0,0 +1,917 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../data/models/user_Info.dart'; +import '../../../data/services/pay_service.dart'; +import '../../../data/services/version_update_service.dart'; +import '../../../data/models/goods.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import '../../../data/services/network/dio_manager.dart'; +import '../../../data/services/network/api.dart'; +import 'package:flutter/foundation.dart'; +import 'dart:async'; +import 'package:app_links/app_links.dart'; + +/// goods 模块基础控制器,封装通用的商品加载与支付流程 +abstract class GoodsBaseController extends GetxController { + final RxList items = [].obs; + final RxBool loading = false.obs; + final RxString error = ''.obs; + final RxBool isPaying = false.obs; + Timer? _payingTimeoutTimer; + + late final PayService _payService = Get.find(); + StreamSubscription? _deeplinkSub; + late final AppLinks _appLinks = AppLinks(); + + @override + void onInit() { + super.onInit(); + _init(); + // 处理冷启动场景:如果 App 通过深链直接启动 + _handleInitialDeepLink(); + } + + @override + void onClose() { + _deeplinkSub?.cancel(); + _deeplinkSub = null; + _payingTimeoutTimer?.cancel(); + _payingTimeoutTimer = null; + super.onClose(); + } + + Future _init() async { + loading.value = true; + error.value = ''; + try { + final list = await fetchProducts(); + items.assignAll(list); + // iOS 环境下,在页面加载后批量查询当前商品的 IAP 详情,便于排查 StoreKit 可用商品 + await _debugQueryIapProductsIfIos(); + } catch (e) { + error.value = e.toString(); + } finally { + loading.value = false; + } + } + + /// 子类实现:返回商品列表(统一使用 DBGoods) + Future> fetchProducts(); + + /// iOS 批量查询当前页面商品的 IAP 详情(仅真机 iOS 生效,Web/Android 跳过) + Future _debugQueryIapProductsIfIos() async { + try { + final isIOS = GetPlatform.isIOS && !kIsWeb; + if (!isIOS) return; + + // 收集页面商品ID;若为空,使用预设的基础套餐ID进行测试 + final ids = items + .map((e) => (e.id ?? '').trim()) + .where((id) => id.isNotEmpty) + .toSet(); + if (ids.isEmpty) { + ids.addAll({ + 'ai_001', + }); + } + + print('[IAP-ios] 批量查询页面商品IDs: ${ids.join(', ')}'); + await _payService.debugQueryProducts(ids); + } catch (e) { + print('[IAP-ios] 批量查询异常: $e'); + } + } + + /// 统一入口:根据后端配置决定是否弹出支付方式选择面板 + Future purchase(DBGoods item) async { + // 读取渠道配置 + final enabled = _getEnabledPayMethods(); + + // 无配置或多个渠道:弹出选择面板 + _PayMethod? method; + if (enabled.length != 1) { + method = await _showPayMethodSheet(enabled); + if (method == null) return; + } else { + // 仅一个渠道:直接走对应支付 + method = enabled.first; + } + + bool ok = false; + String failMsg = '支付失败,请重试'; + isPaying.value = true; + _startPayingTimeout(); + if (method == _PayMethod.wechat) { + ok = await payWechat(item, onResult: (success, msg) { + if (!success) failMsg = msg ?? failMsg; + }); + } else if (method == _PayMethod.alipay) { + ok = await payAlipay(item, onResult: (success, msg) { + if (!success) failMsg = msg ?? failMsg; + }); + } else if (method == _PayMethod.paypal) { + ok = await payPaypal(item, onResult: (success, msg) { + if (!success) failMsg = msg ?? failMsg; + }); + } else if (method == _PayMethod.apple) { + ok = await payAppleIAP(item, onResult: (success, msg) { + if (!success) failMsg = msg ?? failMsg; + }); + } else if (method == _PayMethod.google) { + ok = await payGoogleIAP(item, onResult: (success, msg) { + if (!success) failMsg = msg ?? failMsg; + }); + } + + if (!ok) { + Get.snackbar('提示', failMsg, + snackPosition: SnackPosition.BOTTOM, + margin: const EdgeInsets.all(12)); + } + } + + /// 解析后端 paychannels 并结合平台过滤可用支付方式 + List<_PayMethod> _getEnabledPayMethods() { + try { + final paychannels = + VersionUpdateService.to.cachedAppInfo?.paychannels ?? ''; + final raw = paychannels + .split(',') + .map((e) => e.trim().toLowerCase()) + .where((e) => e.isNotEmpty) + .toList(); + + final bool isIOS = GetPlatform.isIOS && !kIsWeb; + final bool isAndroid = GetPlatform.isAndroid && !kIsWeb; + + final List<_PayMethod> methods = []; + for (final ch in raw) { + switch (ch) { + case 'wxpay': + case 'wechat': + methods.add(_PayMethod.wechat); + break; + case 'alipay': + methods.add(_PayMethod.alipay); + break; + case 'paypal': + methods.add(_PayMethod.paypal); + break; + case 'apple': + if (isIOS) methods.add(_PayMethod.apple); + break; + case 'google': + if (isAndroid) methods.add(_PayMethod.google); + break; + default: + break; + } + } + + // 若后端未下发或全部被平台过滤,则回退为展示所有平台支持的方式 + if (methods.isEmpty) { + final fallback = <_PayMethod>[ + _PayMethod.wechat, + _PayMethod.alipay, + _PayMethod.paypal + ]; + if (isIOS) fallback.add(_PayMethod.apple); + if (isAndroid) fallback.add(_PayMethod.google); + return fallback; + } + return methods; + } catch (_) { + // 任意异常则回退为旧逻辑:展示所有 + final bool isIOS = GetPlatform.isIOS && !kIsWeb; + final bool isAndroid = GetPlatform.isAndroid && !kIsWeb; + final fallback = <_PayMethod>[ + _PayMethod.wechat, + _PayMethod.alipay, + _PayMethod.paypal + ]; + if (isIOS) fallback.add(_PayMethod.apple); + if (isAndroid) fallback.add(_PayMethod.google); + return fallback; + } + } + + /// 底部弹窗:选择支付方式(仅展示后端允许的渠道) + Future<_PayMethod?> _showPayMethodSheet(List<_PayMethod> allowed) async { + final context = Get.context; + final isDark = + context != null && Theme.of(context).brightness == Brightness.dark; + final isIOS = GetPlatform.isIOS && !kIsWeb; + final isAndroid = GetPlatform.isAndroid && !kIsWeb; + return await Get.bottomSheet<_PayMethod>( + SafeArea( + child: Container( + decoration: BoxDecoration( + color: isDark ? Colors.grey[900] : Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: isDark ? Colors.white24 : Colors.black12, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '选择支付方式', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.black, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (allowed.contains(_PayMethod.wechat)) + GestureDetector( + onTap: () => Get.back(result: _PayMethod.wechat), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isDark + ? Colors.white10 + : Colors.black12, + border: Border.all( + color: isDark + ? Colors.white24 + : Colors.black12, + ), + ), + child: Icon(Icons.wechat, + color: Colors.green[600]), + ), + const SizedBox(height: 8), + Text( + '微信支付', + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.white : Colors.black, + ), + ), + ], + ), + ), + if (allowed.contains(_PayMethod.alipay)) + GestureDetector( + onTap: () => Get.back(result: _PayMethod.alipay), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isDark + ? Colors.white10 + : Colors.black12, + border: Border.all( + color: isDark + ? Colors.white24 + : Colors.black12, + ), + ), + child: Icon(Icons.payment, + color: Colors.blue[600]), + ), + const SizedBox(height: 8), + Text( + '支付宝', + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.white : Colors.black, + ), + ), + ], + ), + ), + if (allowed.contains(_PayMethod.paypal)) + GestureDetector( + onTap: () => Get.back(result: _PayMethod.paypal), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isDark + ? Colors.white10 + : Colors.black12, + border: Border.all( + color: isDark + ? Colors.white24 + : Colors.black12, + ), + ), + child: Icon( + Icons.account_balance_wallet, + color: Colors.indigo[600], + ), + ), + const SizedBox(height: 8), + Text( + 'PayPal', + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.white : Colors.black, + ), + ), + ], + ), + ), + if (isIOS && allowed.contains(_PayMethod.apple)) + GestureDetector( + onTap: () => Get.back(result: _PayMethod.apple), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isDark + ? Colors.white10 + : Colors.black12, + border: Border.all( + color: isDark + ? Colors.white24 + : Colors.black12, + ), + ), + child: Icon( + Icons.apple, + color: isDark ? Colors.white : Colors.black, + ), + ), + const SizedBox(height: 8), + Text( + 'Apple 内购', + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.white : Colors.black, + ), + ), + ], + ), + ), + if (isAndroid && allowed.contains(_PayMethod.google)) + GestureDetector( + onTap: () => Get.back(result: _PayMethod.google), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isDark + ? Colors.white10 + : Colors.black12, + border: Border.all( + color: isDark + ? Colors.white24 + : Colors.black12, + ), + ), + child: Icon( + Icons.shop, + color: Colors.green[700], + ), + ), + const SizedBox(height: 8), + Text( + 'Google 内购', + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.white : Colors.black, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + ], + ), + ), + ], + ), + ), + ), + isScrollControlled: false, + ignoreSafeArea: false, + backgroundColor: Colors.transparent, + ); + } + + /// 指定微信支付(直接下单并拉起微信) + Future payWechat(DBGoods item, + {PaymentResultCallback? onResult}) async { + final cents = item.price; // DBGoods.price 单位:分 + final serverUrl = DioManager().baseUrl; + final paymentData = await _payService.createWechatPayOrder( + productId: item.id, + productName: item.name, + amount: cents, + notifyUrl: '$serverUrl/api/home/pay_wechatnotify', + ); + if (paymentData == null) { + onResult?.call(false, '创建微信订单失败'); + isPaying.value = false; + _cancelPayingTimeout(); + return false; + } + final orderId = + paymentData['orderId'] as String? ?? _payService.lastOrderId; + return _payService.wechatPay( + paymentData: paymentData, + onResult: (success, msg) async { + // 先透传给外部回调 + onResult?.call(success, msg); + if (success) { + isPaying.value = false; + _cancelPayingTimeout(); + await _confirmOrderAndShow(orderId, + channel: '微信', initialMessage: msg); + } else { + isPaying.value = false; + _cancelPayingTimeout(); + _showPayResultDialog(false, msg ?? '支付失败'); + } + }, + ); + } + + /// 指定支付宝支付(直接下单并拉起支付宝) + Future payAlipay(DBGoods item, + {PaymentResultCallback? onResult}) async { + final cents = item.price; // DBGoods.price 单位:分 + final orderInfo = await _payService.createAlipayOrder( + productId: item.id, + productName: item.name, + amount: cents, + notifyUrl: 'https://api.ideapsound.com/api/home/pay_alipaynotify', + ); + if (orderInfo == null) { + onResult?.call(false, '创建支付宝订单失败'); + isPaying.value = false; + _cancelPayingTimeout(); + return false; + } + final orderId = _payService.lastOrderId; // 创建订单时已缓存 + return _payService.alipay( + orderInfo: orderInfo, + onResult: (success, msg) async { + onResult?.call(success, msg); + if (success) { + isPaying.value = false; + _cancelPayingTimeout(); + await _confirmOrderAndShow(orderId, + channel: '支付宝', initialMessage: msg); + } else { + isPaying.value = false; + _cancelPayingTimeout(); + _showPayResultDialog(false, msg ?? '支付失败'); + } + }, + ); + } + + /// 指定 Apple 内购(App Store IAP) + Future payAppleIAP(DBGoods item, + {PaymentResultCallback? onResult}) async { + try { + // 先创建服务端订单,记录 order_id 以便后续回查 + final orderId = await _payService.createAppleIapOrder( + productId: item.id, + productName: item.name, + amount: item.price, + ); + if (orderId == null || orderId.isEmpty) { + onResult?.call(false, '创建 Apple 订单失败'); + isPaying.value = false; + _cancelPayingTimeout(); + return false; + } + // 使用商品id作为内购的 productId(与后台保持一致) + final productId = item.id; + // 解析为 App Store 的真实商品ID(支持前缀/映射) + final iosProductId = _payService.resolveAppleProductId(productId); + // 调试:在发起购买前先查询并打印该商品的内购详情/错误,便于定位问题 + await _payService.debugQueryProducts({iosProductId}); + return _payService.processApplePayment( + productId: productId, + onResult: (success, msg) { + // 透传并根据阶段展示 + onResult?.call(success, msg); + if (!success) { + _showPayResultDialog(false, msg); + isPaying.value = false; + _cancelPayingTimeout(); + } else { + // 支付成功或额度待刷新时弹窗,其余阶段用轻提示 + final showDialog = msg.contains('支付成功'); + if (showDialog) { + _showPayResultDialog(true, msg); + // 仅在支付成功阶段,携带校验参数主动查询订单 + _askOrderWithIapVerification(); + isPaying.value = false; + _cancelPayingTimeout(); + } else { + Get.snackbar('提示', msg, + snackPosition: SnackPosition.BOTTOM, + margin: const EdgeInsets.all(12)); + } + } + }, + ); + } catch (e) { + onResult?.call(false, 'Apple 内购异常: $e'); + isPaying.value = false; + _cancelPayingTimeout(); + return false; + } + } + + /// 指定 Google Play 内购(Play Billing IAP) + Future payGoogleIAP(DBGoods item, + {PaymentResultCallback? onResult}) async { + try { + // 先创建服务端订单,记录 order_id 以便后续回查 + final orderId = await _payService.createGoogleIapOrder( + productId: item.id, + productName: item.name, + amount: item.price, + ); + if (orderId == null || orderId.isEmpty) { + onResult?.call(false, '创建 Google 订单失败'); + isPaying.value = false; + _cancelPayingTimeout(); + return false; + } + isPaying.value = true; + _startPayingTimeout(); + + // 使用商品id作为内购的 productId(与后台保持一致) + final productId = item.id; + // 调试:查询并打印当前商品的内购详情,便于确认 ID + await _payService.debugQueryProducts({productId}); + return _payService.processGooglePayment( + productId: productId, + onResult: (success, msg) { + onResult?.call(success, msg); + if (!success) { + _showPayResultDialog(false, msg); + isPaying.value = false; + _cancelPayingTimeout(); + } else { + final showDialog = msg.contains('支付成功'); + if (showDialog) { + _showPayResultDialog(true, msg); + // 仅在支付成功阶段,携带校验参数主动查询订单 + _askOrderWithIapVerification(); + isPaying.value = false; + _cancelPayingTimeout(); + } else { + Get.snackbar('提示', msg, + snackPosition: SnackPosition.BOTTOM, + margin: const EdgeInsets.all(12)); + } + } + }, + ); + } catch (e) { + onResult?.call(false, 'Google 内购异常: $e'); + isPaying.value = false; + _cancelPayingTimeout(); + return false; + } + } + + /// 调试:打印当前页面所有商品的 Google IAP 查询结果 + Future debugPrintGoogleIapProducts() async { + if (!GetPlatform.isAndroid || kIsWeb) { + print('[IAP] 当前平台非 Android 或为 Web,跳过商品查询调试。'); + return; + } + try { + final products = await fetchProducts(); + final ids = products.map((e) => e.id).toSet(); + await _payService.debugQueryProducts(ids); + } catch (e) { + print('[IAP] 调试查询商品异常: $e'); + } + } + + Future _confirmOrderAndShow( + String? orderId, { + required String channel, + String? initialMessage, + int retries = 5, + int intervalMs = 1500, + }) async { + // 若无法获取订单号,直接弹成功提示但提醒稍后刷新 + if (orderId == null || orderId.isEmpty) { + _showPayResultDialog(true, '${channel}支付成功,订单处理中,请稍后查看。'); + return; + } + + String message = initialMessage ?? '${channel}支付成功'; + bool success = false; + try { + // 简单轮询回查,避免回调延迟 + for (int i = 0; i < retries; i++) { + final resp = await Api.payverifyorder({'order_id': orderId}); + print("查询订单 resp: $resp"); + final order = resp?['order'] as Map?; + final user = resp?['user'] as Map?; + if (order != null && user != null) { + final orderStatus = order["status"]; + if (orderStatus == 1) { + User.instance.updateUserInfo(user); + _payService.setVipExptime(User.instance.vipexptime); + success = true; + break; + } + } + await Future.delayed(Duration(milliseconds: intervalMs)); + } + + // 若标记成功但未拿到用户信息,使用token登录接口回拉用户资料,确保额度刷新 + if (success) { + try { + final response = await Api.tokenlogin(); + if (response is Map && response['user'] is Map) { + User.instance + .updateUserInfo(response['user'] as Map); + } + } catch (_) {} + } + } catch (e) { + message = '查询支付结果异常: $e'; + } + _showPayResultDialog(success, message); + } + + /// Apple/Google 内购:无法监听平台回调时,主动查询订单并携带验证数据 + Future _askOrderWithIapVerification() async { + try { + final params = await _payService.buildIapAskParams(); + final resp = await Api.payverifyorder(params); + print('[IAP] 主动查询订单 resp: $resp'); + final order = resp?['order'] as Map?; + final user = resp?['user'] as Map?; + if (order != null) { + final orderStatus = order['status']; + if (orderStatus == 1 && user != null) { + User.instance.updateUserInfo(user); + _payService.setVipExptime(User.instance.vipexptime); + Get.snackbar('提示', '订单已确认,额度已刷新', + snackPosition: SnackPosition.BOTTOM, + margin: const EdgeInsets.all(12)); + return; + } + } + // 若未确认成功,尝试回拉用户信息确保额度同步 + try { + final response = await Api.tokenlogin(); + if (response is Map && response['user'] is Map) { + User.instance + .updateUserInfo(response['user'] as Map); + } + } catch (_) {} + } catch (e) { + print('[IAP] 主动查询订单异常: $e'); + } + } + + void _showPayResultDialog(bool success, String message) { + final context = Get.context; + final isDark = + context != null && Theme.of(context).brightness == Brightness.dark; + Get.dialog( + Dialog( + insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + backgroundColor: isDark ? Colors.grey[900] : Colors.white, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: success + ? (isDark ? Colors.green[800] : Colors.green[100]) + : (isDark ? Colors.red[800] : Colors.red[100]), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + success ? Icons.check_circle : Icons.error, + color: + success ? Colors.green[600] : Colors.redAccent[200], + size: 28, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + success ? '支付成功' : '支付失败', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: isDark ? Colors.white : Colors.black, + ), + ), + const SizedBox(height: 6), + Text( + message, + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.white70 : Colors.grey[700], + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + Divider(color: isDark ? Colors.white24 : Colors.black12), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('知道了'), + ), + ], + ), + ], + ), + ), + ), + barrierDismissible: true, + ); + } + + void _startPayingTimeout({int seconds = 30}) { + _payingTimeoutTimer?.cancel(); + _payingTimeoutTimer = Timer(Duration(seconds: seconds), () { + isPaying.value = false; + _payingTimeoutTimer = null; + }); + } + + void _cancelPayingTimeout() { + _payingTimeoutTimer?.cancel(); + _payingTimeoutTimer = null; + } + + /// 指定 PayPal 支付(创建订单并打开授权链接) + Future payPaypal(DBGoods item, + {PaymentResultCallback? onResult}) async { + final cents = item.price; // DBGoods.price 单位:分 + final approvalUrl = await _payService.createPaypalOrder( + productId: item.id, + productName: item.name, + amount: cents, + notifyUrl: 'https://api.ideapsound.com/api/home/pay_paypalnotify', + ); + if (approvalUrl == null) { + onResult?.call(false, '创建 PayPal 订单失败'); + isPaying.value = false; + _cancelPayingTimeout(); + return false; + } + final orderId = _payService.lastOrderId; // 创建订单时已缓存 + print('[PayPal] 创建完成,orderId: $orderId, approvalUrl: $approvalUrl'); + + // 启动深链监听:当浏览器重定向到 deepvoiceclient://paypal/return 或 /cancel 时处理 + _deeplinkSub?.cancel(); + _deeplinkSub = _appLinks.uriLinkStream.listen((uri) async { + if (uri == null) return; + print('[PayPal] 深链回跳: ${uri.toString()}'); + final token = uri.queryParameters['token'] ?? ''; + if (token.isNotEmpty) { + print('[PayPal] 回跳 token(订单ID): $token'); + } + final scheme = uri.scheme; + final host = uri.host; + final path = uri.path; + if (scheme == 'deepvoiceclient' && host == 'paypal') { + isPaying.value = false; + if (path == '/return') { + isPaying.value = false; + _cancelPayingTimeout(); + print('[PayPal] 检测到 /return,使用 orderId 回查: $orderId'); + await _confirmOrderAndShow( + orderId, + channel: 'PayPal', + initialMessage: '支付完成,正在确认', + retries: 1, + intervalMs: 0, + ); + } else if (path == '/cancel') { + isPaying.value = false; + _cancelPayingTimeout(); + _showPayResultDialog(false, '已取消支付'); + } + // 一次性监听,处理后即取消 + _deeplinkSub?.cancel(); + _deeplinkSub = null; + } + }, onError: (e) { + isPaying.value = false; + _cancelPayingTimeout(); + // 忽略监听错误,保持现有轮询逻辑 + }); + return _payService.paypalPay( + approvalUrl: approvalUrl, + onResult: (success, msg) async { + onResult?.call(success, msg); + if (success) { + // 仅提示已打开授权页面,不在此处回查与弹窗,等待深链回跳后再处理 + print('[PayPal] 已打开授权页面,等待回跳后再确认。orderId: $orderId'); + Get.snackbar('提示', msg ?? '已打开 PayPal,请在浏览器完成支付', + snackPosition: SnackPosition.BOTTOM, + margin: const EdgeInsets.all(12)); + } else { + // 无法打开授权页才提示失败 + _showPayResultDialog(false, msg ?? '无法打开 PayPal 支付页面'); + } + }, + ); + } + + Future _handleInitialDeepLink() async { + try { + final uri = await _appLinks.getInitialLink(); + if (uri == null) return; + print('[PayPal] 冷启动深链: ${uri.toString()}'); + final token = uri.queryParameters['token'] ?? ''; + if (token.isNotEmpty) { + print('[PayPal] 冷启动 token(订单ID): $token'); + } + if (uri.scheme == 'deepvoiceclient' && uri.host == 'paypal') { + if (uri.path == '/return') { + final orderId = _payService.lastOrderId; + if (orderId != null) { + print('[PayPal] 冷启动 /return,使用 orderId 回查: $orderId'); + await _confirmOrderAndShow( + orderId, + channel: 'PayPal', + initialMessage: '支付完成,正在确认', + retries: 1, + intervalMs: 0, + ); + } + } else if (uri.path == '/cancel') { + _showPayResultDialog(false, '已取消支付'); + } + } + } catch (_) { + // 忽略异常,维持原有逻辑 + } + } +} + +enum _PayMethod { wechat, alipay, paypal, apple, google } diff --git a/lib/modules/goods/controllers/goodsai_controller.dart b/lib/modules/goods/controllers/goodsai_controller.dart new file mode 100644 index 000000000..fe04c175c --- /dev/null +++ b/lib/modules/goods/controllers/goodsai_controller.dart @@ -0,0 +1,40 @@ +import 'dart:convert'; +import 'dart:developer' as developer; + +import 'package:get/get.dart'; +import '../../../routes/app_routes.dart'; +import '../../../data/models/goods.dart'; +import '../../../data/services/network/api.dart'; +import 'goods_base_controller.dart'; +// ... existing code ... + +class GoodsaiController extends GoodsBaseController { + @override + Future> fetchProducts() async { + final resp = await Api.getGoods({'usagetype': UsageType.AI.toInt}); + // 输出完整响应,便于调试接口结构 + try { + developer.log('getGoods(AI) resp: ${jsonEncode(resp)}'); + } catch (_) { + developer.log('getGoods(AI) resp (raw): $resp'); + } + if (resp == null || resp is! Map) return []; + if (resp.containsKey('error') || resp.containsKey('msg')) { + final msg = resp['error'] ?? resp['msg'] ?? '服务器错误'; + throw Exception(msg); + } + final container = + (resp is Map && resp['data'] is Map) ? resp['data'] : resp; + final data = (container is Map) ? (container['goodss']) : null; + if (data is List) { + final list = + data.whereType>().map(DBGoods.fromJson).toList(); + // 兜底过滤确保展示 AI 类型 + return list.where((g) => g.usagetype == UsageType.AI).toList(); + } + return []; + } + + // 如需自定义跳转逻辑,可保留重写调用父类 + // Future purchase(DBGoods item) async => super.purchase(item); +} diff --git a/lib/modules/goods/controllers/goodsmeet_controller.dart b/lib/modules/goods/controllers/goodsmeet_controller.dart new file mode 100644 index 000000000..ad171e3c5 --- /dev/null +++ b/lib/modules/goods/controllers/goodsmeet_controller.dart @@ -0,0 +1,30 @@ +import 'package:get/get.dart'; +import '../../../routes/app_routes.dart'; +import '../../../data/models/goods.dart'; +import '../../../data/services/network/api.dart'; +import 'goods_base_controller.dart'; + +class GoodsmeetController extends GoodsBaseController { + @override + Future> fetchProducts() async { + final resp = await Api.getGoods({'usagetype': UsageType.Meet.toInt}); + if (resp == null || resp is! Map) return []; + if (resp.containsKey('error') || resp.containsKey('msg')) { + final msg = resp['error'] ?? resp['msg'] ?? '服务器错误'; + throw Exception(msg); + } + final container = + (resp is Map && resp['data'] is Map) ? resp['data'] : resp; + final data = (container is Map) + ? (container['goodss'] ?? container['goodes'] ?? container['goods']) + : null; + if (data is List) { + final list = + data.whereType>().map(DBGoods.fromJson).toList(); + return list.where((g) => g.usagetype == UsageType.Meet).toList(); + } + return []; + } + + // Future purchase(DBGoods item) async => super.purchase(item); +} diff --git a/lib/modules/goods/controllers/goodstrans_controller.dart b/lib/modules/goods/controllers/goodstrans_controller.dart new file mode 100644 index 000000000..fabab8ae5 --- /dev/null +++ b/lib/modules/goods/controllers/goodstrans_controller.dart @@ -0,0 +1,28 @@ +import 'package:get/get.dart'; +import '../../../routes/app_routes.dart'; +import '../../../data/models/goods.dart'; +import '../../../data/services/network/api.dart'; +import 'goods_base_controller.dart'; + +class GoodstransController extends GoodsBaseController { + @override + Future> fetchProducts() async { + final resp = await Api.getGoods({'usagetype': UsageType.Translation.toInt}); + if (resp == null || resp is! Map) return []; + if (resp.containsKey('error') || resp.containsKey('msg')) { + final msg = resp['error'] ?? resp['msg'] ?? '服务器错误'; + throw Exception(msg); + } + final container = + (resp is Map && resp['data'] is Map) ? resp['data'] : resp; + final data = (container is Map) + ? (container['goodss'] ?? container['goodes'] ?? container['goods']) + : null; + if (data is List) { + final list = + data.whereType>().map(DBGoods.fromJson).toList(); + return list.where((g) => g.usagetype == UsageType.Translation).toList(); + } + return []; + } +} diff --git a/lib/modules/goods/controllers/goodsvip_controller.dart b/lib/modules/goods/controllers/goodsvip_controller.dart new file mode 100644 index 000000000..98b18c030 --- /dev/null +++ b/lib/modules/goods/controllers/goodsvip_controller.dart @@ -0,0 +1,41 @@ +import '../../../data/models/goods.dart'; +import '../../../data/services/network/api.dart'; +import 'goods_base_controller.dart'; + +class GoodsvipController extends GoodsBaseController { + @override + Future> fetchProducts() async { + final types = [ + UsageType.AI, + UsageType.Meet, + UsageType.Translation, + UsageType.Vip, + ]; + final Map all = {}; + for (final t in types) { + final resp = await Api.getGoods({'usagetype': t.toInt}); + if (resp == null || resp is! Map) continue; + if (resp.containsKey('error') || resp.containsKey('msg')) { + final msg = resp['error'] ?? resp['msg'] ?? '服务器错误'; + throw Exception(msg); + } + final container = + (resp is Map && resp['data'] is Map) ? resp['data'] : resp; + final data = (container is Map) + ? (container['goodss'] ?? container['goodes'] ?? container['goods']) + : null; + if (data is List) { + final list = data + .whereType>() + .map(DBGoods.fromJson) + .toList(); + for (final g in list) { + all[g.id] = g; + } + } + } + return all.values.toList(); + } + + // Future purchase(DBGoods item) async => super.purchase(item); +} diff --git a/lib/modules/goods/utils/goods_i18n.dart b/lib/modules/goods/utils/goods_i18n.dart new file mode 100644 index 000000000..7900ce242 --- /dev/null +++ b/lib/modules/goods/utils/goods_i18n.dart @@ -0,0 +1,82 @@ +import 'package:get/get.dart'; +import '../../../data/models/goods.dart'; + +String _localeTag() { + final l = Get.locale; + if (l == null) return 'zh-CN'; + final cc = l.countryCode; + return cc != null && cc.isNotEmpty + ? '${l.languageCode}-${cc.toUpperCase()}' + : l.languageCode; +} + +String localizedGoodsName(DBGoods item) { + final info = item.localInfo(_localeTag()); + final ln = (info['name'] as String?)?.trim() ?? ''; + if (ln.isNotEmpty) return ln; + final id = item.id.trim(); + switch (id) { + case 'ai_001': + return 'ai_001_name'.tr; + case 'ai_002': + return 'ai_002_name'.tr; + case 'ai_003': + return 'ai_003_name'.tr; + case 'meet_001': + return 'meet_001_name'.tr; + case 'meet_002': + return 'meet_002_name'.tr; + case 'meet_003': + return 'meet_003_name'.tr; + case 'trade_001': + return 'trans_001_name'.tr; + case 'trade_002': + return 'trans_002_name'.tr; + case 'trade_003': + return 'trans_003_name'.tr; + default: + final name = item.name.trim(); + return name.isNotEmpty ? name : 'unknown'.tr; + } +} + +String localizedGoodsDesc(DBGoods item) { + final id = item.id.trim(); + + switch (id) { + case 'ai_001': + return 'ai_001_desc'.tr; + case 'ai_002': + return 'ai_002_desc'.tr; + case 'ai_003': + return 'ai_003_desc'.tr; + case 'meet_001': + return 'meet_001_desc'.tr; + case 'meet_002': + return 'meet_002_desc'.tr; + case 'meet_003': + return 'meet_003_desc'.tr; + case 'trade_001': + return 'trans_001_desc'.tr; + case 'trade_002': + return 'trans_002_desc'.tr; + case 'trade_003': + return 'trans_003_desc'.tr; + default: + return ''; + } +} + +String goodsDisplayPrice(DBGoods item) { + final info = item.localInfo(_localeTag()); + final cur = (info['currency'] as String?)?.trim(); + final p = info['price']; + final symbol = (cur != null && cur.isNotEmpty) ? cur : '¥'; + double v; + if (p is num) { + v = p.toDouble(); + } else { + v = item.priceYuan; + } + return '$symbol${v.toStringAsFixed(2)}'; +} diff --git a/lib/modules/goods/views/goodsai_view.dart b/lib/modules/goods/views/goodsai_view.dart new file mode 100644 index 000000000..74dac96bc --- /dev/null +++ b/lib/modules/goods/views/goodsai_view.dart @@ -0,0 +1,494 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import '../../../data/models/goods.dart'; +import '../../../data/models/user_Info.dart'; +import '../controllers/goodsai_controller.dart'; +import '../utils/goods_i18n.dart'; +import 'widgets/goods_footer_section.dart'; + +class GoodsaiView extends GetView { + const GoodsaiView({super.key}); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + elevation: 0.5, + title: Text( + 'aiFunction'.tr, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + ), + body: Obx(() { + // 访问刷新信号以建立依赖关系:用户信息变化时触发重建 + final _ = User.refreshTick.value; + Widget content; + if (controller.loading.value) { + content = const Center(child: CircularProgressIndicator()); + } else if (controller.error.isNotEmpty) { + content = Center(child: Text(controller.error.value)); + } else { + content = ListView( + padding: EdgeInsets.all(16.w), + children: [ + _buildHeader(isDarkMode), + SizedBox(height: 12.h), + _buildUsageCard(isDarkMode), + SizedBox(height: 16.h), + Text( + 'availablePackages'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 8.h), + ...controller.items.map((item) => Padding( + padding: EdgeInsets.only(bottom: 12.h), + child: _buildCard(item, isDarkMode), + )), + SizedBox(height: 16.h), + _buildFeaturesSection(isDarkMode), + SizedBox(height: 16.h), + GoodsFooterSection(isDarkMode: isDarkMode), + ], + ); + } + return Stack( + children: [ + content, + if (controller.isPaying.value) + Positioned.fill( + child: AbsorbPointer( + absorbing: true, + child: Container( + color: Colors.black.withOpacity(0.35), + child: const Center( + child: CircularProgressIndicator(), + ), + ), + ), + ), + ], + ); + }), + ); + } + + Widget _buildHeader(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 44.w, + height: 44.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.indigo[900]!.withOpacity(0.3) + : Colors.indigo[100]!, + borderRadius: BorderRadius.circular(22.r), + ), + child: Icon(Icons.smart_toy, + color: isDarkMode ? Colors.indigo[300]! : Colors.indigo[600]!), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'aiFunction'.tr, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w700, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 6.h), + Text( + 'aiHeaderDesc1'.tr, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + SizedBox(height: 4.h), + Text( + 'aiHeaderDesc2'.tr, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildUsageCard(bool isDarkMode) { + // 从 User 属性计算 aitimer 数据 + int totalAi = 0; + int remainAi = 0; + if (User.isLoggedIn()) { + final u = User.instance; + totalAi = u.aichattotalintegral; + remainAi = u.aichatintegral; + } + // 进度显示为“剩余额度比例”,remain/total: + final progress = (totalAi > 0) ? (remainAi / totalAi).clamp(0.0, 1.0) : 0.0; + + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.timeline, + color: isDarkMode ? Colors.blue[300]! : Colors.blue[600]!), + SizedBox(width: 8.w), + Text( + 'remainingQuota'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 12.h), + LinearProgressIndicator( + value: progress, + minHeight: 8.h, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[300], + valueColor: AlwaysStoppedAnimation( + isDarkMode ? Colors.blue[300]! : Colors.blue[600]!), + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${'totalQuota'.tr}:$totalAi', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + Text( + '${'remaining'.tr}:$remainAi', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildFeaturesSection(bool isDarkMode) { + final textColor = isDarkMode ? Colors.white : Colors.black; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'featureDescription'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + SizedBox(height: 10.h), + MasonryGridView.count( + crossAxisCount: 2, + mainAxisSpacing: 12.h, + crossAxisSpacing: 12.w, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 6, + itemBuilder: (context, index) { + switch (index) { + case 0: + return _buildFeatureCard( + isDarkMode, + icon: Icons.music_note, + title: 'musicAndRecommendation'.tr, + lines: [ + 'musicSearchPlaybackControl'.tr, + 'smartPlaylistAndSimilarRecommendations'.tr, + ], + colorSeed: Colors.purple, + ); + case 1: + return _buildFeatureCard( + isDarkMode, + icon: Icons.navigation, + title: 'navigationAndRoutes'.tr, + lines: [ + 'routePlanningAndCongestionAvoidance'.tr, + 'voiceBroadcastAndWaypoints'.tr, + ], + colorSeed: Colors.orange, + ); + case 2: + return _buildFeatureCard( + isDarkMode, + icon: Icons.wb_sunny, + title: 'weatherQuery'.tr, + lines: [ + 'realTimeWeatherForecast'.tr, + 'clothingAdviceAndTravelReminders'.tr, + ], + colorSeed: Colors.blue, + ); + case 3: + return _buildFeatureCard( + isDarkMode, + icon: Icons.place, + title: 'nearbySearch'.tr, + lines: [ + 'poiSearchFoodGasParking'.tr, + 'ratingsHoursAndDirectRoutes'.tr, + ], + colorSeed: Colors.green, + ); + case 4: + return _buildFeatureCard( + isDarkMode, + icon: Icons.newspaper, + title: 'newsInformation'.tr, + lines: [ + 'hotNewsAndTopicOverview'.tr, + 'longArticleSummariesAndKeyPoints'.tr, + ], + colorSeed: Colors.indigo, + ); + default: + return _buildFeatureCard( + isDarkMode, + icon: Icons.call, + title: 'communicationAssistant'.tr, + lines: [ + 'helpCallAndSMS'.tr, + 'templatedMessagesAndScheduleReminders'.tr, + ], + colorSeed: Colors.teal, + ); + } + }, + ), + SizedBox(height: 12.h), + Text( + 'aiDisclaimer'.tr, + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ); + } + + Widget _buildFeatureCard( + bool isDarkMode, { + required IconData icon, + required String title, + required List lines, + required MaterialColor colorSeed, + }) { + final bgColor = isDarkMode ? Colors.grey[900] : Colors.white; + final mainColor = isDarkMode ? colorSeed[300]! : colorSeed[600]!; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + return Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 36.w, + height: 36.w, + decoration: BoxDecoration( + color: isDarkMode + ? mainColor.withOpacity(0.25) + : mainColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(18.r), + ), + child: Icon(icon, color: mainColor), + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + SizedBox(height: 8.h), + ...lines.map((line) => Padding( + padding: EdgeInsets.only(bottom: 4.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.check_circle, color: mainColor, size: 14.sp), + SizedBox(width: 6.w), + Expanded( + child: Text( + line, + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ), + ], + ), + )), + ], + ), + ); + } + + Widget _buildCard(DBGoods item, bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + width: 42.w, + height: 42.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.teal[900]!.withOpacity(0.3) + : Colors.teal[100]!, + borderRadius: BorderRadius.circular(21.r), + ), + child: Icon(Icons.auto_awesome, + color: isDarkMode ? Colors.teal[300]! : Colors.teal[600]!), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(localizedGoodsName(item), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + )), + SizedBox(height: 4.h), + Text(localizedGoodsDesc(item), + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[600], + )), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(goodsDisplayPrice(item), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + )), + SizedBox(height: 8.h), + GestureDetector( + onTap: () => controller.purchase(item), + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: Colors.blue[600], + borderRadius: BorderRadius.circular(20.r), + ), + child: Text( + 'purchase'.tr, + style: TextStyle(fontSize: 12.sp, color: Colors.white), + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/modules/goods/views/goodsmeet_view.dart b/lib/modules/goods/views/goodsmeet_view.dart new file mode 100644 index 000000000..a32c306c5 --- /dev/null +++ b/lib/modules/goods/views/goodsmeet_view.dart @@ -0,0 +1,433 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../../data/models/goods.dart'; +import '../../../data/models/user_Info.dart'; +import '../controllers/goodsmeet_controller.dart'; +import '../utils/goods_i18n.dart'; +import 'widgets/goods_footer_section.dart'; + +class GoodsmeetView extends GetView { + const GoodsmeetView({super.key}); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + size: 20.sp, + color: isDarkMode ? Colors.white : Colors.black87, + ), + onPressed: () => Get.back(), + ), + title: Text( + 'meetingAssistantRecharge'.tr, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + ), + body: Obx(() { + // 访问刷新信号以建立依赖关系:用户信息变化时触发重建 + final _ = User.refreshTick.value; + if (controller.loading.value) { + return const Center(child: CircularProgressIndicator()); + } + if (controller.error.isNotEmpty) { + return Center(child: Text(controller.error.value)); + } + return ListView.separated( + padding: EdgeInsets.all(16.w), + itemCount: controller.items.length + 4, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (_, index) { + if (index == 0) { + return _buildHeader(isDarkMode); + } + if (index == 1) { + return _buildUsageCard(isDarkMode); + } + final itemIndex = index - 2; + if (itemIndex < controller.items.length) { + final item = controller.items[itemIndex]; + return _buildCard(item, isDarkMode); + } + if (index == controller.items.length + 2) { + return _buildFeaturesSection(isDarkMode); + } + return GoodsFooterSection(isDarkMode: isDarkMode); + }, + ); + }), + ); + } + + Widget _buildHeader(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 44.w, + height: 44.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.pink[900]!.withOpacity(0.3) + : Colors.pink[100]!, + borderRadius: BorderRadius.circular(22.r), + ), + child: Icon(Icons.people_alt, + color: isDarkMode ? Colors.pink[300]! : Colors.pink[600]!), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'conferenceAssistant'.tr, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w700, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 6.h), + Text( + 'meetingHeaderDesc1'.tr, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + SizedBox(height: 4.h), + Text( + 'meetingHeaderDesc2'.tr, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildUsageCard(bool isDarkMode) { + int totalAi = 0; + int remainAi = 0; + if (User.isLoggedIn()) { + final u = User.instance; + totalAi = u.meettotalintegral; + remainAi = u.meetintegral; + } + final progress = (totalAi > 0) ? (remainAi / totalAi).clamp(0.0, 1.0) : 0.0; + + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.timeline, + color: isDarkMode ? Colors.blue[300]! : Colors.blue[600]!), + SizedBox(width: 8.w), + Text( + 'remainingQuota'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 12.h), + LinearProgressIndicator( + value: progress, + minHeight: 8.h, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[300], + valueColor: AlwaysStoppedAnimation( + isDarkMode ? Colors.blue[300]! : Colors.blue[600]!), + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${'totalQuota'.tr}:$totalAi', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + Text( + '${'remaining'.tr}:$remainAi', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildCard(DBGoods item, bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + width: 42.w, + height: 42.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.pink[900]!.withOpacity(0.3) + : Colors.pink[100]!, + borderRadius: BorderRadius.circular(21.r), + ), + child: Icon(Icons.people_alt, + color: isDarkMode ? Colors.pink[300]! : Colors.pink[600]!), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(localizedGoodsName(item), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + )), + // SizedBox(height: 4.h), + // Text(localizedGoodsDesc(item), + // style: TextStyle( + // fontSize: 12.sp, + // color: isDarkMode ? Colors.white70 : Colors.grey[600], + // )), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(goodsDisplayPrice(item), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + )), + SizedBox(height: 8.h), + GestureDetector( + onTap: () => controller.purchase(item), + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: Colors.blue[600], + borderRadius: BorderRadius.circular(20.r), + ), + child: Text( + 'purchase'.tr, + style: TextStyle(fontSize: 12.sp, color: Colors.white), + ), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildFeaturesSection(bool isDarkMode) { + final textColor = isDarkMode ? Colors.white : Colors.black; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'featureDescription'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + SizedBox(height: 10.h), + Wrap( + spacing: 12.w, + runSpacing: 12.h, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + children: [ + _buildFeatureCard( + isDarkMode, + icon: Icons.mic, + title: 'meetingRecording'.tr, + lines: ['meetingAutoRecording'.tr, 'meetingSpeakerTagging'.tr], + colorSeed: Colors.pink, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.summarize, + title: 'transcriptionAndSummary'.tr, + lines: [ + 'meetingLongAudioTranscription'.tr, + 'meetingKeyPointSummary'.tr + ], + colorSeed: Colors.blue, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.search, + title: 'keywordSearch'.tr, + lines: ['meetingTopicNameSearch'.tr, 'meetingTimelineJump'.tr], + colorSeed: Colors.indigo, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.share, + title: 'collaborationSharing'.tr, + lines: [ + 'meetingExportSummary'.tr, + 'meetingMultiPlatformShare'.tr + ], + colorSeed: Colors.teal, + ), + ], + ), + SizedBox(height: 12.h), + Text( + 'meetingDisclaimer'.tr, + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ); + } + + Widget _buildFeatureCard( + bool isDarkMode, { + required IconData icon, + required String title, + required List lines, + required MaterialColor colorSeed, + }) { + final bgColor = isDarkMode ? Colors.grey[900] : Colors.white; + final mainColor = isDarkMode ? colorSeed[300]! : colorSeed[600]!; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + final width = (Get.width - 16.w * 2 - 12.w) / 2; + return Container( + width: width, + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 36.w, + height: 36.w, + decoration: BoxDecoration( + color: isDarkMode + ? mainColor.withOpacity(0.25) + : mainColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(18.r), + ), + child: Icon(icon, color: mainColor), + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + ], + ), + SizedBox(height: 8.h), + ...lines.map((l) => Padding( + padding: EdgeInsets.only(bottom: 4.h), + child: + Text(l, style: TextStyle(fontSize: 12.sp, color: subColor)), + )), + ], + ), + ); + } +} diff --git a/lib/modules/goods/views/goodstrans_view.dart b/lib/modules/goods/views/goodstrans_view.dart new file mode 100644 index 000000000..9ac127551 --- /dev/null +++ b/lib/modules/goods/views/goodstrans_view.dart @@ -0,0 +1,435 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../../data/models/goods.dart'; +import '../../../data/models/user_Info.dart'; +import '../controllers/goodstrans_controller.dart'; +import '../utils/goods_i18n.dart'; +import 'widgets/goods_footer_section.dart'; + +class GoodstransView extends GetView { + const GoodstransView({super.key}); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + size: 20.sp, + color: isDarkMode ? Colors.white : Colors.black87, + ), + onPressed: () => Get.back(), + ), + title: Text( + 'translationRecharge'.tr, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + ), + body: Obx(() { + final _ = User.refreshTick.value; + if (controller.loading.value) { + return const Center(child: CircularProgressIndicator()); + } + if (controller.error.isNotEmpty) { + return Center(child: Text(controller.error.value)); + } + return ListView.separated( + padding: EdgeInsets.all(16.w), + itemCount: controller.items.length + 4, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (_, index) { + if (index == 0) { + return _buildHeader(isDarkMode); + } + if (index == 1) { + return _buildUsageCard(isDarkMode); + } + final itemIndex = index - 2; + if (itemIndex < controller.items.length) { + final item = controller.items[itemIndex]; + return _buildCard(item, isDarkMode); + } + if (index == controller.items.length + 2) { + return _buildFeaturesSection(isDarkMode); + } + return GoodsFooterSection(isDarkMode: isDarkMode); + }, + ); + }), + ); + } + + Widget _buildHeader(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 44.w, + height: 44.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.indigo[900]!.withOpacity(0.3) + : Colors.indigo[100]!, + borderRadius: BorderRadius.circular(22.r), + ), + child: Icon(Icons.translate, + color: isDarkMode ? Colors.indigo[300]! : Colors.indigo[600]!), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'interpretation'.tr, //翻译 + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w700, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 6.h), + Text( + 'transHeaderDesc1'.tr, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + SizedBox(height: 4.h), + Text( + 'transHeaderDesc2'.tr, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildUsageCard(bool isDarkMode) { + int totalAi = 0; + int remainAi = 0; + if (User.isLoggedIn()) { + final u = User.instance; + totalAi = u.tradetotalintegral; + remainAi = u.tradeintegral; + } + final progress = (totalAi > 0) ? (remainAi / totalAi).clamp(0.0, 1.0) : 0.0; + + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.timeline, + color: isDarkMode ? Colors.blue[300]! : Colors.blue[600]!), + SizedBox(width: 8.w), + Text( + 'remainingQuota'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 12.h), + LinearProgressIndicator( + value: progress, + minHeight: 8.h, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[300], + valueColor: AlwaysStoppedAnimation( + isDarkMode ? Colors.blue[300]! : Colors.blue[600]!), + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${'totalQuota'.tr}:$totalAi', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + Text( + '${'remaining'.tr}:$remainAi', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildCard(DBGoods item, bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + width: 42.w, + height: 42.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.indigo[900]!.withOpacity(0.3) + : Colors.indigo[100]!, + borderRadius: BorderRadius.circular(21.r), + ), + child: Icon(Icons.translate, + color: isDarkMode ? Colors.indigo[300]! : Colors.indigo[600]!), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(localizedGoodsName(item), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + )), + // SizedBox(height: 4.h), + // Text(localizedGoodsDesc(item), + // style: TextStyle( + // fontSize: 12.sp, + // color: isDarkMode ? Colors.white70 : Colors.grey[600], + // )), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(goodsDisplayPrice(item), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + )), + SizedBox(height: 8.h), + GestureDetector( + onTap: () => controller.purchase(item), + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: Colors.blue[600], + borderRadius: BorderRadius.circular(20.r), + ), + child: Text( + 'purchase'.tr, //购买 + style: TextStyle(fontSize: 12.sp, color: Colors.white), + ), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildFeaturesSection(bool isDarkMode) { + final textColor = isDarkMode ? Colors.white : Colors.black; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'featureDescription'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + SizedBox(height: 10.h), + Wrap( + spacing: 12.w, + runSpacing: 12.h, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + children: [ + _buildFeatureCard( + isDarkMode, + icon: Icons.translate, + title: 'realtimeTranscription'.tr, + lines: ['translateZhEn'.tr, 'lowLatencyVoiceBroadcast'.tr], + colorSeed: Colors.indigo, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.text_snippet, + title: 'textTranslation'.tr, + lines: [ + 'multiLangTextTranslation'.tr, + 'termOptimizationContextPreserve'.tr + ], + colorSeed: Colors.blue, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.library_books, + title: 'sceneLexicon'.tr, + lines: [ + 'lexiconTourBusinessLife'.tr, + 'personalVocabularyMemory'.tr + ], + colorSeed: Colors.teal, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.subtitles, + title: 'subtitleDisplay'.tr, + lines: [ + 'conversationSubtitleLargeText'.tr, + 'colorHighContrast'.tr + ], + colorSeed: Colors.purple, + ), + ], + ), + SizedBox(height: 12.h), + Text( + 'transDisclaimer'.tr, + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ); + } + + Widget _buildFeatureCard( + bool isDarkMode, { + required IconData icon, + required String title, + required List lines, + required MaterialColor colorSeed, + }) { + final bgColor = isDarkMode ? Colors.grey[900] : Colors.white; + final mainColor = isDarkMode ? colorSeed[300]! : colorSeed[600]!; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + final width = (Get.width - 16.w * 2 - 12.w) / 2; + return Container( + width: width, + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 36.w, + height: 36.w, + decoration: BoxDecoration( + color: isDarkMode + ? mainColor.withOpacity(0.25) + : mainColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(18.r), + ), + child: Icon(icon, color: mainColor), + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + ], + ), + SizedBox(height: 8.h), + ...lines.map((l) => Padding( + padding: EdgeInsets.only(bottom: 4.h), + child: + Text(l, style: TextStyle(fontSize: 12.sp, color: subColor)), + )), + ], + ), + ); + } +} diff --git a/lib/modules/goods/views/goodsvip_view.dart b/lib/modules/goods/views/goodsvip_view.dart new file mode 100644 index 000000000..4cc881e3b --- /dev/null +++ b/lib/modules/goods/views/goodsvip_view.dart @@ -0,0 +1,890 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../../data/models/goods.dart'; +import '../../../data/models/user_Info.dart'; +import '../controllers/goodsvip_controller.dart'; +import '../utils/goods_i18n.dart'; +import 'widgets/goods_footer_section.dart'; + +class GoodsvipView extends GetView { + const GoodsvipView({super.key}); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + foregroundColor: Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + size: 20.sp, + color: isDarkMode ? Colors.white : Colors.black87, + ), + onPressed: () => Get.back(), + ), + title: Text( + 'aiRecharge'.tr, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + ), + body: Obx(() { + final userTick = User.refreshTick.value; + Widget content; + if (controller.loading.value) { + content = const Center(child: CircularProgressIndicator()); + } else if (controller.error.isNotEmpty) { + content = Center(child: Text(controller.error.value)); + } else { + final Map> grouped = {}; + for (final g in controller.items) { + grouped.putIfAbsent(g.usagetype, () => []).add(g); + } + final order = [UsageType.Vip]; + final children = []; + children.add(_buildFeaturesSection(isDarkMode)); + children.add(SizedBox(height: 16.h)); + // children.add(_buildScenesSection(isDarkMode)); + // children.add(SizedBox(height: 16.h)); + for (int i = 0; i < order.length; i++) { + final t = order[i]; + final list = grouped[t]; + if (list == null || list.isEmpty) continue; + children.add(_buildTypeSection(t, list, isDarkMode)); + if (i != order.length - 1) { + children.add(SizedBox(height: 16.h)); + children.add( + Divider(color: isDarkMode ? Colors.white24 : Colors.black12)); + children.add(SizedBox(height: 16.h)); + } + } + + children.add(GoodsFooterSection(isDarkMode: isDarkMode)); + content = ListView( + key: ValueKey(userTick), + padding: EdgeInsets.all(16.w), + children: children, + ); + } + return Stack( + children: [ + content, + if (controller.isPaying.value) + Positioned.fill( + child: AbsorbPointer( + absorbing: true, + child: Container( + color: Colors.black.withOpacity(0.35), + child: const Center( + child: CircularProgressIndicator(), + ), + ), + ), + ), + ], + ); + }), + ); + } + + Widget _buildFeaturesSection(bool isDarkMode) { + final textColor = isDarkMode ? Colors.white : Colors.black; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'featureDescription'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + SizedBox(height: 10.h), + Wrap( + spacing: 12.w, + runSpacing: 12.h, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + children: [ + _buildFeatureCard( + isDarkMode, + icon: Icons.workspace_premium, + title: 'benefitsOverview'.tr, + lines: ['higherQuota'.tr, 'lowerUnitPrice'.tr], + colorSeed: Colors.amber, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.support_agent, + title: 'priorityService'.tr, + lines: ['exclusiveSupportChannel'.tr, 'fastResponse'.tr], + colorSeed: Colors.purple, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.local_offer, + title: 'eventsAndDiscounts'.tr, + lines: ['memberExclusiveEvents'.tr, 'longTermDiscountPlan'.tr], + colorSeed: Colors.indigo, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.auto_awesome, + title: 'generalPrivileges'.tr, + lines: ['earlyAccessNewFeatures'.tr, 'crossDeviceSync'.tr], + colorSeed: Colors.teal, + ), + ], + ), + SizedBox(height: 12.h), + Text( + 'vipDisclaimer'.tr, + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ); + } + + Widget _buildFeatureCard( + bool isDarkMode, { + required IconData icon, + required String title, + required List lines, + required MaterialColor colorSeed, + }) { + final bgColor = isDarkMode ? Colors.grey[900] : Colors.white; + final mainColor = isDarkMode ? colorSeed[300]! : colorSeed[600]!; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + final width = (Get.width - 16.w * 2 - 12.w) / 2; + return Container( + width: width, + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 36.w, + height: 36.w, + decoration: BoxDecoration( + color: isDarkMode + ? mainColor.withOpacity(0.25) + : mainColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(18.r), + ), + child: Icon(icon, color: mainColor), + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + ], + ), + SizedBox(height: 8.h), + ...lines.map((l) => Padding( + padding: EdgeInsets.only(bottom: 4.h), + child: + Text(l, style: TextStyle(fontSize: 12.sp, color: subColor)), + )), + ], + ), + ); + } + + Widget _buildTypeHeader(UsageType type, bool isDarkMode) { + final titles = { + UsageType.AI: 'aiFunction'.tr, + UsageType.Meet: 'conferenceAssistant'.tr, + UsageType.Translation: 'simultaneousTranslation'.tr, + UsageType.Vip: 'memberService'.tr, + }; + final iconAndColor = _iconColorForType(type, isDarkMode); + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + String titleStr = titles[type] ?? type.name; + String? vipSubtitle; + if (type == UsageType.Vip) { + int viplv = 0; + int vipexptime = 0; + if (User.isLoggedIn()) { + final u = User.instance; + viplv = u.viplv; + vipexptime = u.vipexptime; + } + DateTime? expiry; + if (vipexptime > 0) { + expiry = vipexptime > 1000000000000 + ? DateTime.fromMillisecondsSinceEpoch(vipexptime) + : DateTime.fromMillisecondsSinceEpoch(vipexptime * 1000); + } + // titleStr = 'Vip $viplv'; + vipSubtitle = expiry != null + ? '${'expiryLabel'.tr}${_fmtDate(expiry)}' + : 'notActivated'.tr; + } + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Container( + // width: 44.w, + // height: 44.w, + // decoration: BoxDecoration( + // color: iconAndColor.background, + // borderRadius: BorderRadius.circular(22.r), + // ), + // child: Icon(iconAndColor.icon, color: iconAndColor.foreground), + // ), + // SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + titleStr, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w700, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 8.h), + if (type == UsageType.Vip && vipSubtitle != null) + Text( + vipSubtitle, + style: TextStyle(fontSize: 12.sp, color: subColor), + ) + else + _buildMetricsInline(type, isDarkMode), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTypeSection( + UsageType type, List list, bool isDarkMode) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTypeHeader(type, isDarkMode), + SizedBox(height: 8.h), + _buildGoodsGrid(list, type, isDarkMode), + ], + ); + } + + Widget _buildMetricsInline(UsageType type, bool isDarkMode) { + final textColor = isDarkMode ? Colors.white : Colors.black; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + if (type == UsageType.Vip) { + int viplv = 0; + int vipexptime = 0; + if (User.isLoggedIn()) { + final u = User.instance; + viplv = u.viplv; + vipexptime = u.vipexptime; + } + DateTime? expiry; + if (vipexptime > 0) { + expiry = vipexptime > 1000000000000 + ? DateTime.fromMillisecondsSinceEpoch(vipexptime) + : DateTime.fromMillisecondsSinceEpoch(vipexptime * 1000); + } + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Lv.$viplv', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + SizedBox(height: 4.h), + Text( + expiry != null + ? '${'validityPeriod'.tr}${_fmtDate(expiry)}' + : 'notBound'.tr, + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ), + ), + // Container( + // padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h), + // decoration: BoxDecoration( + // color: isDarkMode ? Colors.white10 : Colors.black12, + // borderRadius: BorderRadius.circular(16.r), + // ), + // child: Text( + // viplv > 0 ? 'enabled'.tr : 'disabled'.tr, + // style: TextStyle(fontSize: 11.sp, color: subColor), + // ), + // ), + ], + ); + } + + int total = 0; + int remain = 0; + if (User.isLoggedIn()) { + final u = User.instance; + if (type == UsageType.AI) { + total = u.aichattotalintegral; + remain = u.aichatintegral; + } else if (type == UsageType.Meet) { + total = u.meettotalintegral; + remain = u.meetintegral; + } else if (type == UsageType.Translation) { + total = u.tradetotalintegral; + remain = u.tradeintegral; + } + } + final progress = (total > 0) ? (remain / total).clamp(0.0, 1.0) : 0.0; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LinearProgressIndicator( + value: progress, + minHeight: 8.h, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[300], + valueColor: AlwaysStoppedAnimation( + isDarkMode ? Colors.blue[300]! : Colors.blue[600]!), + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${'totalQuota'.tr}:$total', + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + Text( + '${'remaining'.tr}:$remain', + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ), + ], + ); + } + + Widget _buildGoodsCardByType(DBGoods item, UsageType type, bool isDarkMode) { + final iconAndColor = _iconColorForType(type, isDarkMode); + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + width: 42.w, + height: 42.w, + decoration: BoxDecoration( + color: iconAndColor.background, + borderRadius: BorderRadius.circular(21.r), + ), + child: Icon(iconAndColor.icon, color: iconAndColor.foreground), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(localizedGoodsName(item), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + )), + SizedBox(height: 4.h), + Text(localizedGoodsDesc(item), + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[600], + )), + ], + ), + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(goodsDisplayPrice(item), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + )), + SizedBox(height: 8.h), + GestureDetector( + onTap: () => controller.purchase(item), + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: Colors.blue[600], + borderRadius: BorderRadius.circular(20.r), + ), + child: Text( + 'purchase'.tr, + style: TextStyle( + fontSize: 12.sp, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildMetricsForType(UsageType type, bool isDarkMode) { + if (type == UsageType.Vip) { + return _buildVipMetricsCard(isDarkMode); + } + return _buildQuotaCard(type, isDarkMode); + } + + Widget _buildVipMetricsCard(bool isDarkMode) { + final textColor = isDarkMode ? Colors.white : Colors.black; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + int viplv = 0; + int vipexptime = 0; + if (User.isLoggedIn()) { + final u = User.instance; + viplv = u.viplv; + vipexptime = u.vipexptime; + } + DateTime? expiry; + if (vipexptime > 0) { + expiry = vipexptime > 1000000000000 + ? DateTime.fromMillisecondsSinceEpoch(vipexptime) + : DateTime.fromMillisecondsSinceEpoch(vipexptime * 1000); + } + final iconColors = _iconColorForType(UsageType.Vip, isDarkMode); + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + width: 42.w, + height: 42.w, + decoration: BoxDecoration( + color: iconColors.background, + borderRadius: BorderRadius.circular(21.r), + ), + child: Icon(iconColors.icon, color: iconColors.foreground), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Lv.$viplv', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + SizedBox(height: 4.h), + Text( + expiry != null + ? '${'validityPeriod'.tr}${_fmtDate(expiry)}' + : 'notBound'.tr, + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white10 : Colors.black12, + borderRadius: BorderRadius.circular(16.r), + ), + child: Text( + viplv > 0 ? 'enabled'.tr : 'disabled'.tr, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + ), + ], + ), + ); + } + + Widget _buildQuotaCard(UsageType type, bool isDarkMode) { + int total = 0; + int remain = 0; + if (User.isLoggedIn()) { + final u = User.instance; + if (type == UsageType.AI) { + total = u.aichattotalintegral; + remain = u.aichatintegral; + } else if (type == UsageType.Meet) { + total = u.meettotalintegral; + remain = u.meetintegral; + } else if (type == UsageType.Translation) { + total = u.tradetotalintegral; + remain = u.tradeintegral; + } + } + final progress = (total > 0) ? (remain / total).clamp(0.0, 1.0) : 0.0; + final iconColors = _iconColorForType(type, isDarkMode); + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + final textColor = isDarkMode ? Colors.white : Colors.black; + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 36.w, + height: 36.w, + decoration: BoxDecoration( + color: iconColors.background, + borderRadius: BorderRadius.circular(18.r), + ), + child: Icon(iconColors.icon, color: iconColors.foreground), + ), + SizedBox(width: 8.w), + Text( + 'remainingQuota'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + ], + ), + SizedBox(height: 12.h), + LinearProgressIndicator( + value: progress, + minHeight: 8.h, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[300], + valueColor: AlwaysStoppedAnimation( + isDarkMode ? Colors.blue[300]! : Colors.blue[600]!), + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${'totalQuota'.tr}:$total', + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + Text( + '${'remaining'.tr}:$remain', + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ), + ], + ), + ); + } + + Widget _buildGoodsGrid(List list, UsageType type, bool isDarkMode) { + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 8.h, + crossAxisSpacing: 12.w, + childAspectRatio: 1.3, + ), + itemCount: list.length, + itemBuilder: (_, i) => _buildGridItemCard(list[i], type, isDarkMode), + ); + } + + Widget _buildGridItemCard(DBGoods item, UsageType type, bool isDarkMode) { + final iconColors = _iconColorForType(type, isDarkMode); + return Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 34.w, + height: 20.w, + decoration: BoxDecoration( + color: iconColors.background, + borderRadius: BorderRadius.circular(17.r), + ), + child: Icon(iconColors.icon, color: iconColors.foreground), + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + goodsDisplayPrice(item), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 21.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + ], + ), + SizedBox(height: 6.h), + Expanded( + child: Text( + localizedGoodsName(item), + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 15.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + ), + // SizedBox(height: 6.h), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + GestureDetector( + onTap: () => controller.purchase(item), + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h), + decoration: BoxDecoration( + color: Colors.blue[600], + borderRadius: BorderRadius.circular(8.r), + ), + child: Text( + 'purchase'.tr, + style: TextStyle(fontSize: 12.sp, color: Colors.white), + ), + ), + ), + ], + ), + ], + ), + ); + } + + String _fmtDate(DateTime d) { + final y = d.year.toString().padLeft(4, '0'); + final m = d.month.toString().padLeft(2, '0'); + final day = d.day.toString().padLeft(2, '0'); + return '$y-$m-$day'; + } + + _IconColors _iconColorForType(UsageType type, bool isDarkMode) { + switch (type) { + case UsageType.AI: + return _IconColors( + icon: Icons.smart_toy, + background: isDarkMode + ? Colors.teal[900]!.withOpacity(0.3) + : Colors.teal[100]!, + foreground: isDarkMode ? Colors.teal[300]! : Colors.teal[600]!, + ); + case UsageType.Meet: + return _IconColors( + icon: Icons.people_alt, + background: isDarkMode + ? Colors.pink[900]!.withOpacity(0.3) + : Colors.pink[100]!, + foreground: isDarkMode ? Colors.pink[300]! : Colors.pink[600]!, + ); + case UsageType.Translation: + return _IconColors( + icon: Icons.translate, + background: isDarkMode + ? Colors.indigo[900]!.withOpacity(0.3) + : Colors.indigo[100]!, + foreground: isDarkMode ? Colors.indigo[300]! : Colors.indigo[600]!, + ); + case UsageType.Vip: + case UsageType.UsageNull: + return _IconColors( + icon: Icons.workspace_premium, + background: isDarkMode + ? Colors.amber[900]!.withOpacity(0.3) + : Colors.amber[100]!, + foreground: isDarkMode ? Colors.amber[300]! : Colors.amber[600]!, + ); + } + } + + Widget _buildScenesSection(bool isDarkMode) { + final textColor = isDarkMode ? Colors.white : Colors.black; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'scene'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + SizedBox(height: 10.h), + Wrap( + spacing: 12.w, + runSpacing: 12.h, + children: [ + _buildFeatureCard( + isDarkMode, + icon: Icons.smart_toy, + title: 'aiFunction'.tr, + lines: ['aiHeaderDesc1'.tr, 'aiHeaderDesc2'.tr], + colorSeed: Colors.teal, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.people_alt, + title: 'conferenceAssistant'.tr, + lines: ['meetingHeaderDesc1'.tr, 'meetingHeaderDesc2'.tr], + colorSeed: Colors.pink, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.translate, + title: 'simultaneousTranslation'.tr, + lines: ['transHeaderDesc1'.tr, 'transHeaderDesc2'.tr], + colorSeed: Colors.indigo, + ), + ], + ), + SizedBox(height: 12.h), + Text( + 'vipDisclaimer'.tr, + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ); + } +} + +class _IconColors { + final IconData icon; + final Color background; + final Color foreground; + const _IconColors({ + required this.icon, + required this.background, + required this.foreground, + }); +} diff --git a/lib/modules/goods/views/widgets/goods_footer_section.dart b/lib/modules/goods/views/widgets/goods_footer_section.dart new file mode 100644 index 000000000..00a7afabf --- /dev/null +++ b/lib/modules/goods/views/widgets/goods_footer_section.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../../../data/models/appconfig.dart'; + +class GoodsFooterSection extends StatelessWidget { + final bool isDarkMode; + const GoodsFooterSection({super.key, required this.isDarkMode}); + + @override + Widget build(BuildContext context) { + final subColor = isDarkMode ? Colors.white60 : Colors.grey[600]; + final qq = AppConfig.env('iapCustomerServiceQQ') ?? ''; + return Padding( + padding: EdgeInsets.symmetric(vertical: 12.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: double.infinity, + child: Divider(color: isDarkMode ? Colors.white24 : Colors.black12), + ), + SizedBox(height: 8.h), + Text( + 'appNameProvider'.tr, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + SizedBox(height: 4.h), + if (qq.isNotEmpty) ...[ + Text( + 'iapCustomerServiceQQLabel'.trParams({'qq': qq}), + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + SizedBox(height: 4.h), + ], + // SizedBox(height: 4.h), + Text( + 'copyrightVoitrans'.tr, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + ], + ), + ); + } +} diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 466b4792b..a7729e491 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -277,7 +277,7 @@ class HomeController extends GetxController { // 连接成功后检查OTA升级 if (noviceGuideService.isHomeGuide) { - Future.delayed(const Duration(seconds: 2), () { + Future.delayed(const Duration(seconds: 1), () { _checkOtaUpgrade(); }); } @@ -542,9 +542,7 @@ class HomeController extends GetxController { } // 使用默认耳机图片 - return isDarkMode - ? 'assets/images/headphone_new3.png' - : 'assets/images/headphone_new4.png'; + return 'assets/images/defaultPicture.png'; } /// 判断是否为网络图片 diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index 6be0315bd..04c3d7de0 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -85,10 +85,12 @@ class HomeView extends GetView { // 顶部标题栏 _buildAppBar(isDarkMode), + SizedBox(height: 10.h), + // 顶部图片 _buildEarbudsImage(), - SizedBox(height: 20.h), + SizedBox(height: 10.h), // 设备状态卡片 IntroStepBuilder( @@ -101,7 +103,7 @@ class HomeView extends GetView { overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, - '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言', + 'guideWelcomeWakeAI'.tr, onNext: () { controller.noviceGuideService.startWakeupListen(); }, @@ -210,26 +212,10 @@ class HomeView extends GetView { height: 220.h, child: PageView( children: [ - // Center( - // child: ClipRRect( - // // 添加 ClipRRect 来实现圆角 - // borderRadius: BorderRadius.circular(16.r), // 使用与其他部件一致的圆角大小 - // child: SizedBox( - // width: double.infinity, - // height: 200.h, - // child: Image.asset( - // isDarkMode - // ? 'assets/images/robot_light.png' - // : 'assets/images/robot_light.png', - // fit: BoxFit.fitWidth, - // ), - // ), - // ), - // ), Center( child: ClipRRect( // 添加 ClipRRect - borderRadius: BorderRadius.circular(16.r), + borderRadius: BorderRadius.circular(20.r), child: GetBuilder( builder: (controller) { final imagePath = controller.getDeviceImagePath(isDarkMode); @@ -238,16 +224,16 @@ class HomeView extends GetView { return isNetwork ? Image.network( imagePath, + width: double.infinity, height: 220.h, fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) { // 网络图片加载失败时使用默认图片 return Image.asset( - isDarkMode - ? 'assets/images/headphone_new3.png' - : 'assets/images/headphone_new4.png', + 'assets/images/defaultPicture.png', + width: double.infinity, height: 220.h, - fit: BoxFit.contain, + fit: BoxFit.cover, ); }, loadingBuilder: (context, child, loadingProgress) { @@ -268,16 +254,16 @@ class HomeView extends GetView { ) : Image.asset( imagePath, + width: double.infinity, height: 220.h, - fit: BoxFit.contain, + fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { // 本地图片加载失败时使用默认图片 return Image.asset( - isDarkMode - ? 'assets/images/headphone_new3.png' - : 'assets/images/headphone_new4.png', + 'assets/images/defaultPicture.png', + width: double.infinity, height: 220.h, - fit: BoxFit.contain, + fit: BoxFit.cover, ); }, ); @@ -285,22 +271,6 @@ class HomeView extends GetView { ), ), ), - // Center( - // child: ClipRRect( - // // 添加 ClipRRect - // borderRadius: BorderRadius.circular(16.r), - // child: SizedBox( - // width: double.infinity, - // height: 200.h, - // child: Image.asset( - // isDarkMode - // ? 'assets/images/glasses_light.png' - // : 'assets/images/glasses_light.png', - // fit: BoxFit.fitWidth, - // ), - // ), - // ), - // ), ], ), ); @@ -812,7 +782,7 @@ class HomeView extends GetView { overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, - '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧', + 'guideAwakenSuccess'.tr, onTap: () { params.onFinish(); controller.exploreAIFeatures(); diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index 0773d62c4..ff506e4a9 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -113,18 +113,24 @@ class LoginController extends GetxController { _contactController = TextEditingController(); _verificationCodeController = TextEditingController(); + _initDeviceInfo(); + _initAppInfo(); // 新增:初始化APP信息 + _checkGuestLoginConfig(); // 检查游客登录配置 + + final channel = versionService.cachedChannel; isChinaMainland.value = - Get.deviceLocale?.countryCode?.toUpperCase() == 'CN'; //是否中国大陆地区 + Get.deviceLocale?.countryCode?.toUpperCase() == 'CN' && + channel != UserChannel.GLOBAL_GOOGLE; //是否中国大陆地区 // 初始化默认国家代码 _initDefaultCountryCode(); - _hasAcceptedPrivacy.value = - _storage.read('hasAcceptedPrivacy') ?? false; // 从本地存储中读取是否已接受隐私政策 - - _initDeviceInfo(); - _initAppInfo(); // 新增:初始化APP信息 - _checkGuestLoginConfig(); // 检查游客登录配置 + if (channel != UserChannel.CHINA_HUAWEI && + channel != UserChannel.CHINA_HONOR) { + // 非中国华为和荣耀渠道,检查是否已接受隐私政策 + _hasAcceptedPrivacy.value = + _storage.read('hasAcceptedPrivacy') ?? false; // 从本地存储中读取是否已接受隐私政策 + } } @override diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index aa679d154..927bb9f96 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -9,12 +9,16 @@ import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; import 'package:path_provider/path_provider.dart'; import '../../../core/utils/synchrodata.dart'; import '../../../data/services/db/sqflite_api.dart'; import '../../../data/services/meeting/meeting_task_service.dart'; +import '../../../data/services/meeting/meeting_task_service2.dart'; import '../../../data/services/meeting/meeting_upload_service.dart'; +import '../../../data/services/meeting/summary_task_service.dart'; +import '../../../data/services/meeting/meeting_task_aggregator.dart'; import '../model/meeting_details_model.dart'; import '../model/meeting_model.dart'; @@ -27,9 +31,12 @@ class MeetingDetailsController extends GetxController late AnimationController lottieController; late final Worker _tasktypeListener; + late final Worker _tasktypeListenerV2; final _meetingUploadService = Get.find(); final _meetingTaskService = Get.find(); + final _meetingTaskService2 = Get.find(); + final _summaryTaskService = Get.find(); final PlayerController playerController = PlayerController(); @@ -88,17 +95,38 @@ class MeetingDetailsController extends GetxController ..duration = const Duration(seconds: 2); final Map args = Get.arguments ?? {}; id = args['id']; + final storage = GetStorage(); + final String svc = + (storage.read("meeting_service_type_$id") ?? '').toString(); + final bool isFast = svc == 'fast'; _meetingTaskService.meetingId = id; + _meetingTaskService2.meetingId = id; + Get.find().setMeeting(id); _getDataDetails(); _loadJS(); _tasktypeListener = ever(_meetingTaskService.tasktype, (value) { + if (isFast && value != 3) return; meetingDetails.value.tasktype = value; if (_everType <= value) { - if (value == 2) { + if (!isFast && value == 2) { textList.value = _meetingTaskService.textList; } else if (value == 3) { - meetingDetails.value.summary = _meetingTaskService.summary; - meetingDetails.value.overview = _meetingTaskService.overview; + meetingDetails.value.summary = _summaryTaskService.summary; + meetingDetails.value.overview = _summaryTaskService.overview; + summaryNumberModifications.value++; + } + } + meetingDetails.refresh(); + }); + _tasktypeListenerV2 = ever(_meetingTaskService2.tasktype, (value) { + if (!isFast) return; + meetingDetails.value.tasktype = value; + if (_everType <= value) { + if (value == 2) { + textList.value = _meetingTaskService2.textList; + } else if (value == 3) { + meetingDetails.value.summary = _summaryTaskService.summary; + meetingDetails.value.overview = _summaryTaskService.overview; summaryNumberModifications.value++; } } @@ -109,24 +137,29 @@ class MeetingDetailsController extends GetxController @override void onClose() { _tasktypeListener.dispose(); + _tasktypeListenerV2.dispose(); _currentDurationSubscription?.cancel(); _playerStateSubscription?.cancel(); tabController.dispose(); playerController.dispose(); lottieController.dispose(); + Get.find().clearMeeting(); super.onClose(); } void popInvokedWithResult(didPop, result) async { _meetingUploadService.removeUpload(id); _meetingTaskService.removeTask(id); + _meetingTaskService2.removeTask(id); _meetingTaskService.meetingId = 0; + _meetingTaskService2.meetingId = 0; } void _getDataDetails() async { final data = await SqfliteApi.getMeetingDetails(id); meetingData.value = MeetingModel.fromJson(data); meetingDetails.value = MeetingDetailsModel.fromJson(data['details']); + _everType = meetingDetails.value.tasktype + 1; _meetingTaskService.tasktype.value = meetingDetails.value.tasktype; if (meetingDetails.value.tasktype >= 3) { dragOffset.value = 0; @@ -254,10 +287,33 @@ class MeetingDetailsController extends GetxController bool isSpeaker, String language, String prompt, String outline) async { EasyLoading.show(); _everType = 2; - String audiourl = meetingData.value.audiourl; - String taskId = meetingDetails.value.taskid; - await _meetingTaskService.addTask( - id, audiourl, taskId, isSpeaker, language, prompt, outline); + final String audiourl = meetingData.value.audiourl; + final int seconds = meetingData.value.seconds; + final storage = GetStorage(); + storage.write("meeting_service_type_$id", + (seconds > 0 && seconds <= 7200) ? 'fast' : 'standard'); + if (seconds > 0 && seconds <= 7200) { + await _meetingTaskService2.addTask( + id, + audiourl, + isSpeaker: isSpeaker, + language: language, + targetLanguage: '', + prompt: prompt, + outline: outline, + ); + } else { + final String taskId = meetingDetails.value.taskid; + await _meetingTaskService.addTask( + id, + audiourl, + taskId, + isSpeaker, + language, + prompt, + outline, + ); + } meetingDetails.value.tasktype = 1; meetingDetails.refresh(); EasyLoading.dismiss(); @@ -272,8 +328,20 @@ class MeetingDetailsController extends GetxController text += '${element['content']}\n'; } _everType = 3; + EasyLoading.show(); await SqfliteApi.editMeeting(id, {'tasktype': 2}); - _meetingTaskService.tasktype.value = 2; - _meetingTaskService.aiServiceMessage(id, text, prompt, outline); + meetingDetails.value.tasktype = 2; + meetingDetails.refresh(); + final storage = GetStorage(); + final String svc = + (storage.read("meeting_service_type_$id") ?? '').toString(); + final bool isFast = svc == 'fast'; + if (isFast) { + _meetingTaskService2.tasktype.value = 2; + } else { + _meetingTaskService.tasktype.value = 2; + } + _summaryTaskService.addTaskList(id, text, prompt, outline); + EasyLoading.dismiss(); } } diff --git a/lib/modules/meeting/controllers/meeting_home_controller.dart b/lib/modules/meeting/controllers/meeting_home_controller.dart index 28317cfbd..7eaab1b26 100644 --- a/lib/modules/meeting/controllers/meeting_home_controller.dart +++ b/lib/modules/meeting/controllers/meeting_home_controller.dart @@ -19,7 +19,10 @@ import '../../../core/utils/permission_util.dart'; import 'package:share_plus/share_plus.dart'; import '../../../data/services/meeting/meeting_task_service.dart'; +import '../../../data/services/meeting/meeting_task_service2.dart'; +import '../../../data/services/meeting/meeting_task_aggregator.dart'; import '../../../data/services/meeting/meeting_upload_service.dart'; +import '../../../data/services/meeting/summary_task_service.dart'; import '../../../routes/app_routes.dart'; import 'meeting_controller.dart'; @@ -28,6 +31,9 @@ class MeetingHomeController extends GetxController { final meetingController = Get.find(); final meetingUploadService = Get.find(); final meetingTaskService = Get.find(); + final meetingTaskService2 = Get.find(); + final taskAggregator = Get.find(); + final summaryTaskService = Get.find(); final AudioPlayer _audioPlayer = AudioPlayer(); @@ -404,7 +410,8 @@ class MeetingHomeController extends GetxController { Logger.error('importFailed' .tr .replaceAll('{filePath}', filePath) - .replaceAll('{error}', e.toString())); // 翻译键代表的中文文本内容 + .replaceAll('{error}', + e.toString())); // 对应中文:导入文件失败: {filePath}, error: {error} } } else { effectivePath = filePath; @@ -583,6 +590,7 @@ class MeetingHomeController extends GetxController { _deleteFile(filepath); meetingUploadService.removeUpload(id, isDeleteTask: true); meetingTaskService.removeTask(id, isDeleteTask: true); + meetingTaskService2.removeTask(id, isDeleteTask: true); } // 删除文件 diff --git a/lib/modules/meeting/controllers/mind_map_controller.dart b/lib/modules/meeting/controllers/mind_map_controller.dart index 997ec75bb..09b91b613 100644 --- a/lib/modules/meeting/controllers/mind_map_controller.dart +++ b/lib/modules/meeting/controllers/mind_map_controller.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -19,6 +20,8 @@ class MindMapController extends GetxController { final double _width = 1.sw - 25.w; bool isWebViewReady = false; + int imageType = 1; + late final Worker _summaryNumberListener; @override @@ -170,18 +173,29 @@ class MindMapController extends GetxController { final String dataUrl = message.message; final String base64Str = dataUrl.split(',').last; final Uint8List bytes = base64.decode(base64Str); - final String fileName = - '${_meetingDetailsController.meetingData.value.title}_mindmap.png'; - final tempDir = await getTemporaryDirectory(); - final File file = File('${tempDir.path}/$fileName'); - await file.writeAsBytes(bytes); - final result = await SharePlus.instance.share(ShareParams( - text: 'shareMindMap'.tr, //对应中文:分享思维导图 - - files: [XFile(file.path)], - )); - if (result.status == ShareResultStatus.success) { - EasyLoading.showToast('shareSuccess'.tr); //对应中文:分享成功 + if (imageType == 1) { + // 保存图片 + final result = await ImageGallerySaverPlus.saveImage( + bytes, + quality: 100, + ); + if (result['isSuccess']) { + EasyLoading.showToast('保存成功'); + } + } else { + // 分享图片 + final String fileName = + '${_meetingDetailsController.meetingData.value.title}_mindmap.png'; + final tempDir = await getTemporaryDirectory(); + final File file = File('${tempDir.path}/$fileName'); + await file.writeAsBytes(bytes); + final result = await SharePlus.instance.share(ShareParams( + text: 'shareMindMap'.tr, //对应中文:分享思维导图 + files: [XFile(file.path)], + )); + if (result.status == ShareResultStatus.success) { + EasyLoading.showToast('shareSuccess'.tr); //对应中文:分享成功 + } } } } diff --git a/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart index e929fdfcf..e8268169d 100644 --- a/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart @@ -7,6 +7,8 @@ import 'package:get_storage/get_storage.dart'; import '../../controllers/meeting_details_controller.dart'; import 'record_language_bottom_sheet.dart'; import '../template/template_view.dart'; +import '../../../../data/models/user_Info.dart'; +import '../../../common/widgets/recharge_dialog.dart'; class GenerateBottomSheet extends StatefulWidget { final bool isSummarizeAgain; @@ -300,6 +302,10 @@ class _GenerateBottomSheetState extends State { padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), child: GestureDetector( onTap: () { + if (User.isLoggedIn() && User.instance.meetintegral <= 0) { + showRechargeDialog('meeting'); + return; + } if (_templateData['id'] != _templateList[0]['id']) { int index = _templateList .indexWhere((v) => v['id'] == _templateData['id']); diff --git a/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart index 7705997f9..be5f16eb9 100644 --- a/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart @@ -10,10 +10,12 @@ import 'package:get_storage/get_storage.dart'; import 'package:htmltopdfwidgets/htmltopdfwidgets.dart' as pdf; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; + import 'package:webview_flutter/webview_flutter.dart'; import '../../../../core/utils/synchrodata.dart'; import '../../../../data/services/version_update_service.dart'; +import '../../../../data/services/network/dio_manager.dart'; import '../../controllers/meeting_details_controller.dart'; import '../../controllers/mind_map_controller.dart'; import 'second_level_bottom_sheet.dart'; @@ -102,7 +104,7 @@ class _ShareBottomSheetState extends State { // ); // 获取动态服务器地址 - final serverUrl = VersionUpdateService.to.cachedServerUrl; + final serverUrl = DioManager().baseUrl; final serverHost = serverUrl .replaceFirst('https://', '') .replaceFirst('http://', ''); @@ -209,6 +211,33 @@ class _ShareBottomSheetState extends State { ); } : null), + _shareItem( + Icons.save_alt, + '保存思维导图', + onTap: _controller.meetingDetails.value.tasktype >= 3 + ? () { + Get.back(); + final mindMapController = + Get.find(); + mindMapController.imageType = 1; + if (mindMapController.isWebViewReady) { + mindMapController.webViewController + .runJavaScript('captureImage()'); + } else { + mindMapController.webViewController + .setNavigationDelegate( + NavigationDelegate( + onPageFinished: (String url) { + mindMapController.isWebViewReady = true; + mindMapController.webViewController + .runJavaScript('captureImage()'); + }, + ), + ); + } + } + : null, + ), _shareItem( Icons.lan, 'shareMindMap'.tr, // 对应中文:分享思维导图 @@ -218,6 +247,7 @@ class _ShareBottomSheetState extends State { Get.back(); final mindMapController = Get.find(); + mindMapController.imageType = 2; if (mindMapController.isWebViewReady) { mindMapController.webViewController .runJavaScript('captureImage()'); diff --git a/lib/modules/meeting/views/meeting_home_view.dart b/lib/modules/meeting/views/meeting_home_view.dart index 81cd3b22b..36c5d610e 100644 --- a/lib/modules/meeting/views/meeting_home_view.dart +++ b/lib/modules/meeting/views/meeting_home_view.dart @@ -10,6 +10,7 @@ import '../controllers/meeting_home_controller.dart'; import '../model/meeting_model.dart'; import '../utils/meeting_ui_utils.dart'; import 'bottomSheet/edit_title_bottom_sheet.dart'; +import '../../../../data/models/user_Info.dart'; ///会议主页面,提供会议列表、会议详情、会议设置 class MeetingHomeView extends StatefulWidget { @@ -173,6 +174,60 @@ class _MeetingHomeViewState extends State ), ), centerTitle: true, + bottom: PreferredSize( + preferredSize: Size.fromHeight(28.w), + child: Obx(() { + final _ = User.refreshTick.value; + final remain = User.isLoggedIn() ? User.instance.meetintegral : 0; + final total = User.isLoggedIn() ? User.instance.meettotalintegral : 0; + final double fraction = + total > 0 ? (remain / total).clamp(0.0, 1.0).toDouble() : 0.0; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: EdgeInsets.only(right: 12.w, bottom: 4.w), + alignment: Alignment.centerRight, + child: Text( + _formatHms(remain), + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ), + LayoutBuilder( + builder: (context, constraints) { + final barWidth = constraints.maxWidth * fraction; + return Stack( + children: [ + Container( + height: 2.w, + decoration: BoxDecoration( + color: isDarkMode ? Colors.white10 : Colors.black12, + borderRadius: BorderRadius.circular(2.w), + ), + ), + Align( + alignment: Alignment.centerRight, + child: Container( + width: barWidth, + height: 2.w, + decoration: BoxDecoration( + color: MeetingUIUtils.getButtonColor(isDarkMode), + borderRadius: BorderRadius.circular(2.w), + ), + ), + ), + ], + ); + }, + ), + ], + ); + }), + ), ); } @@ -321,7 +376,7 @@ class _MeetingHomeViewState extends State }), Obx(() { Map taskData = - _controller.meetingTaskService.taskFinishList.firstWhere( + _controller.taskAggregator.taskFinishList.firstWhere( (element) => element['id'] == data.id, orElse: () => {}, ); @@ -456,4 +511,14 @@ class _MeetingHomeViewState extends State return '${hours}h ${remainingMinutes}m ${remainingSeconds}s'; } } + + String _formatHms(int seconds) { + final sec = seconds < 0 ? 0 : seconds; // 确保秒数非负 + final d = Duration(seconds: sec); + final h = d.inHours; + final m = d.inMinutes.remainder(60); + final s = d.inSeconds.remainder(60); + String twoDigits(int n) => n.toString().padLeft(2, '0'); + return '${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}'; + } } diff --git a/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart b/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart index cd05687f0..146645d90 100644 --- a/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart +++ b/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart @@ -525,17 +525,20 @@ class OtaCoreService { } } + product.isforceupdate = product.version?.contains("+"); + return { - 'latestVersion': product.version ?? "", + 'latestVersion': product.version?.split('+').first ?? "", 'updateDescription': product.updatedescription ?? "", 'updatePackageAddress': product.updatepackageaddress ?? "", + 'isForceUpdate': product.isforceupdate ?? false, }; } /// 比较版本号,判断是否有新版本可用 /// [currentVersion] 当前版本号 /// [latestVersion] 最新版本号 - /// 返回true表示有新版本可用(支持升级和降级) + /// 返回true表示有新版本可用(只支持升级) bool isNewVersionAvailable(String currentVersion, String latestVersion) { try { List currentParts = @@ -555,10 +558,12 @@ class OtaCoreService { latestParts.add(0); } - // 比较每一段,只要有不同就返回true(支持升级和降级) + // 从高位到低位比较版本号 for (int i = 0; i < maxLength; i++) { - if (latestParts[i] != currentParts[i]) { + if (latestParts[i] > currentParts[i]) { return true; + } else if (latestParts[i] < currentParts[i]) { + return false; } } diff --git a/lib/modules/ota_upgrade/views/ota_upgrade_view.dart b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart index 18dc7485f..1bc1caf0f 100644 --- a/lib/modules/ota_upgrade/views/ota_upgrade_view.dart +++ b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart @@ -13,7 +13,7 @@ class OtaUpgradeView extends GetView { return WillPopScope( onWillPop: () async { - if (controller.isUpgrade.value) { + if (controller.isUpgrade.value && controller.showDetailedStatus.value) { // 显示确认弹窗 await Get.dialog( AlertDialog( @@ -54,7 +54,8 @@ class OtaUpgradeView extends GetView { color: isDarkMode ? Colors.white : Colors.black, ), onPressed: () { - if (controller.isUpgrade.value) { + if (controller.isUpgrade.value && + controller.showDetailedStatus.value) { // 显示确认弹窗 Get.dialog( AlertDialog( @@ -172,7 +173,7 @@ class OtaUpgradeView extends GetView { // 设备图标 Widget _buildDeviceIcon(bool isDarkMode) { return Container( - padding: EdgeInsets.all(16.w), + padding: EdgeInsets.all(6.w), decoration: BoxDecoration( color: isDarkMode ? Colors.white.withValues(alpha: 0.05) @@ -190,46 +191,52 @@ class OtaUpgradeView extends GetView { final imagePath = controller.lastConnectedDeviceImage; if (imagePath == null || imagePath.isEmpty) { - return Image.asset( - isDarkMode - ? 'assets/images/headphone_new3.png' - : 'assets/images/headphone_new4.png', - height: 100.h, - fit: BoxFit.contain, + return ClipRRect( + borderRadius: BorderRadius.circular(12.r), + child: SizedBox( + width: 120.h, + height: 120.h, + child: Image.asset( + 'assets/images/defaultPicture.jpg', + fit: BoxFit.cover, + ), + ), ); } - // 网络图片 - return Image.network( - imagePath, - height: 100.h, - fit: BoxFit.contain, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return SizedBox( - height: 100.h, - child: Center( - child: CircularProgressIndicator( - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - strokeWidth: 2, - color: isDarkMode ? Colors.white : Colors.blue, - ), - ), - ); - }, - errorBuilder: (context, error, stackTrace) { - // 网络图片加载失败时使用默认图片 - return Image.asset( - isDarkMode - ? 'assets/images/headphone_new3.png' - : 'assets/images/headphone_new4.png', - height: 100.h, + // 网络图片(正方形裁剪) + return ClipRRect( + borderRadius: BorderRadius.circular(16.r), + child: SizedBox( + width: 120.h, + height: 120.h, + child: Image.network( + imagePath, fit: BoxFit.contain, - ); - }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return SizedBox( + height: 100.h, + child: Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + strokeWidth: 2, + color: isDarkMode ? Colors.white : Colors.blue, + ), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + 'assets/images/defaultPicture.jpg', + fit: BoxFit.cover, + ); + }, + ), + ), ); }, ), @@ -827,7 +834,7 @@ class OtaUpgradeView extends GetView { // 更新按钮 Widget _buildUpdateButton(bool isDarkMode) { return Obx(() { - if (controller.isUpgrade.value) { + if (controller.isUpgrade.value && controller.showDetailedStatus.value) { return _buildProgressIndicator(isDarkMode); } diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 44200e5fb..2b63098c6 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -18,6 +18,7 @@ import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/ble_manager.dart'; import '../../../core/utils/permission_util.dart'; import '../../../data/services/version_update_service.dart'; +import '../../../data/models/channel_app_model.dart'; /// 配对状态枚举 - 简化版本 enum PairingState { @@ -137,9 +138,12 @@ class PairingController extends GetxController with WidgetsBindingObserver { _startInitialCheck(); }); - final app_authcode_jump = AppConfig.env('APP_AUTHCODE_JUMP') ?? ""; + // final app_authcode_jump = AppConfig.env('APP_AUTHCODE_JUMP') ?? ""; //print('app_authcode_jump: $app_authcode_jump'); - if (app_authcode_jump == "true") { + // 如果APP_AUTHCODE_JUMP为true,或者用户已登录且有绑定设备,启用跳过按钮 + final allowSkipDeviceBinding = versionService.cachedAllowSkipDeviceBinding; + if (allowSkipDeviceBinding || + (User.isLoggedIn() && User.instance.devices.isNotEmpty)) { isShowSkipButton = true; // 启用跳过按钮 } } @@ -512,7 +516,7 @@ class PairingController extends GetxController with WidgetsBindingObserver { /// 开始初始检查 void _startInitialCheck() { - _transition(PairingState.checking); + // _transition(PairingState.checking); _checkCurrentStatus(); } @@ -566,15 +570,20 @@ class PairingController extends GetxController with WidgetsBindingObserver { return _bluetoothManager.bluetoothState != 'off'; } - /// 检查是否为华为设备 + /// 检查是否为华为设备或者华为渠道 bool _isHuaweiDevice() { final deviceBrand = versionService.cachedDeviceBrand; + final channel = versionService.cachedChannel; return deviceBrand.isNotEmpty && - (deviceBrand.contains('huawei') || deviceBrand.contains('华为')); + (deviceBrand.contains('huawei') || deviceBrand.contains('华为')) || + (channel == UserChannel.CHINA_HUAWEI); } /// 检查权限 Future _checkPermissions() async { + if (isHuaweiDevice) { + isHuaweiHasChecked = _storage.read('isHuaweiHasChecked') ?? false; + } //如果是华为设备,且已经检查过,直接返回权限状态 if (isHuaweiDevice && isHuaweiHasChecked) { final bluetoothScanStatus = await Permission.bluetoothScan.status; @@ -1048,6 +1057,55 @@ class PairingController extends GetxController with WidgetsBindingObserver { _transition(PairingState.verifying); _verifyDevice(_currentClassicDevice!); } + /* + message UserBindDeviceResp { + DBUserDivice device = 1; //绑定的耳机信息 + bool isgiveaway = 2; //@go_tags(`gorm:"isgiveaway" json:"isgiveaway"`) 是否是第一次绑定 + int32 viplv = 3; //@go_tags(`gorm:"viplv:50" json:"viplv"`) viplv + int64 vipexptime = 4; //@go_tags(`gorm:"vipexptime" json:"vipexptime"`) 会员过期时间 + int64 aichatintegral = 5; //@go_tags(`gorm:"aichatintegral" json:"aichatintegral"`) ai聊天积分 + int64 aichattotalintegral = 6; //@go_tags(`gorm:"aichattotalintegral" json:"aichattotalintegral"`) ai聊天总积分 + int64 tradeintegral = 7; //@go_tags(`gorm:"tradeintegral" json:"tradeintegral"`) 翻译积分 + int64 tradetotalintegral = 8; //@go_tags(`gorm:"tradetotalintegral" json:"tradetotalintegral"`) 翻译总积分 + int64 meetintegral = 9; //@go_tags(`gorm:"meettotalintegral" json:"meettotalintegral"`) 会议积分 + int64 meettotalintegral = 10; //@go_tags(`gorm:"meettotalintegral" json:"meettotalintegral"`) 会议总积分 + } + */ + // 需要读取返回的数据判断 isgiveaway 是否 为 true,是的话 需要读取 后续的数据 更新到用户数据上面去 + if (response['isgiveaway'] == true) { + User.instance.viplv = response['viplv'] ?? 0; + User.instance.vipexptime = response['vipexptime'] ?? 0; + User.instance.aichatintegral = response['aichatintegral'] ?? 0; + User.instance.aichattotalintegral = + response['aichattotalintegral'] ?? 0; + User.instance.tradeintegral = response['tradeintegral'] ?? 0; + User.instance.tradetotalintegral = response['tradetotalintegral'] ?? 0; + User.instance.meetintegral = response['meetintegral'] ?? 0; + User.instance.meettotalintegral = response['meettotalintegral'] ?? 0; + final lv = User.instance.viplv; + final vipexptime = User.instance.vipexptime; + DateTime? expiry; + if (vipexptime > 0) { + expiry = DateTime.fromMillisecondsSinceEpoch(vipexptime * 1000); + } + final y = expiry?.year.toString().padLeft(4, '0') ?? ''; + final m = expiry?.month.toString().padLeft(2, '0') ?? ''; + final d = expiry?.day.toString().padLeft(2, '0') ?? ''; + final expiryText = expiry != null ? '$y-$m-$d' : 'N/A'; + Get.dialog( + AlertDialog( + title: Text('bindRewardTitle'.tr), + content: Text('bindRewardSimpleDesc'.tr), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text('ok'.tr), + ), + ], + ), + barrierDismissible: true, + ); + } return; } else { Logger.e(_tag, '绑定设备失败!'); @@ -1132,13 +1190,13 @@ class PairingController extends GetxController with WidgetsBindingObserver { Logger.e(_tag, '启动AgentService失败: $e'); } //print('lxm-上一个路由: ${Get.previousRoute.toString()}'); - if (Get.previousRoute == Routes.home) { - // 如果是从home页过来的,则直接返回 - Get.back(); - } else { - // 否则,跳转到home页并清空之前的路由栈 - Get.offAllNamed(Routes.home); - } + // if (Get.previousRoute == Routes.home) { + // 如果是从home页过来的,则直接返回 + // Get.back(); + // } else { + // 否则,跳转到home页并清空之前的路由栈 + Get.offAllNamed(Routes.home); + // } // }); } diff --git a/lib/modules/pairing/views/pairing_view.dart b/lib/modules/pairing/views/pairing_view.dart index 2a0a21ee5..141a8b4a4 100644 --- a/lib/modules/pairing/views/pairing_view.dart +++ b/lib/modules/pairing/views/pairing_view.dart @@ -487,7 +487,7 @@ class _PairingViewState extends State case ErrorType.bluetoothOff: return 'connectDeepSoundViaSystemBluetooth'.tr; // 蓝牙已关闭,请打开蓝牙 case ErrorType.noPermission: - return 'lackPermissions'.tr; // 缺少蓝牙权限 + return 'lackPermissions'.tr; // 缺少蓝牙权限和位置权限 case ErrorType.noClassicDevice: return 'connectDeepSoundViaSystemBluetooth' .tr; // 请先通过系统蓝牙连接Voitrans设备 diff --git a/lib/modules/permissions/controllers/permissions_controller.dart b/lib/modules/permissions/controllers/permissions_controller.dart index fb69e9b1d..1b184a4e4 100644 --- a/lib/modules/permissions/controllers/permissions_controller.dart +++ b/lib/modules/permissions/controllers/permissions_controller.dart @@ -145,7 +145,6 @@ class PermissionsController extends GetxController { Permission.calendarFullAccess, Permission.location, Permission.phone, - Permission.sms, Permission.contacts, ...bluetoothPermissions, Permission.notification, diff --git a/lib/modules/permissions/models/permission_group.dart b/lib/modules/permissions/models/permission_group.dart index 30fddf8a5..7b7990dbe 100644 --- a/lib/modules/permissions/models/permission_group.dart +++ b/lib/modules/permissions/models/permission_group.dart @@ -49,7 +49,7 @@ class PermissionGroup { const PermissionGroupModel( titleKey: 'communicationPermissions', icon: Icons.phone_android, - permissions: [Permission.phone, Permission.sms, Permission.contacts], + permissions: [Permission.phone, Permission.contacts], ), // 蓝牙权限组 - 根据平台动态获取权限 PermissionGroupModel( diff --git a/lib/modules/picture_translation/controllers/picture_translation_controller.dart b/lib/modules/picture_translation/controllers/picture_translation_controller.dart index 9f25c0350..abd55af47 100644 --- a/lib/modules/picture_translation/controllers/picture_translation_controller.dart +++ b/lib/modules/picture_translation/controllers/picture_translation_controller.dart @@ -34,8 +34,8 @@ class PictureTranslationController extends GetxController { final isProcessing = false.obs; final pickedImageFile = Rx(null); final currentStep = ProcessingStep( - title: '等待操作', - description: '请选择图片开始翻译', + title: 'waitingForAction'.tr, // 等待操作 + description: 'pleaseSelectImageToStart'.tr, // 请选择图片开始翻译 status: ProcessingStatus.idle, ).obs; @@ -152,13 +152,17 @@ class PictureTranslationController extends GetxController { if (source == ImageSource.camera) { final status = await Permission.camera.request(); if (!status.isGranted) { - Get.snackbar('权限需求', '需要相机权限才能拍照'); + Get.snackbar( + 'permissionRequired'.tr, // 权限需求 + 'cameraPermissionRequired'.tr); // 需要相机权限才能拍照 return false; } } else { final status = await Permission.photos.request(); if (!status.isGranted) { - Get.snackbar('权限需求', '需要相册权限才能选择图片'); + Get.snackbar( + 'permissionRequired'.tr, // 权限需求 + 'photoAlbumPermissionRequired'.tr); // 需要相册权限才能选择图片 return false; } } @@ -194,8 +198,10 @@ class PictureTranslationController extends GetxController { try { isProcessing.value = true; _updateStep( - '选择图片', - '正在打开${source == ImageSource.camera ? '相机' : '相册'}...', + 'chooseImage'.tr, // 选择图片 + (source == ImageSource.camera + ? 'openingCamera'.tr // 正在打开相机... + : 'openingPhotoAlbum'.tr), // 正在打开相册... ProcessingStatus.picking); final XFile? image = await _imagePicker.pickImage( @@ -206,7 +212,10 @@ class PictureTranslationController extends GetxController { ); if (image == null) { - _updateStep('已取消', '用户取消了选择', ProcessingStatus.idle); + _updateStep( + 'cancelled'.tr, // 已取消 + 'userCancelledSelection'.tr, // 用户取消了选择 + ProcessingStatus.idle); return; } @@ -217,23 +226,39 @@ class PictureTranslationController extends GetxController { pickedImageFile.value = file; // 直接调用图片翻译API(包含识别+回贴) - _updateStep('翻译图片', '正在调用图片翻译服务...', ProcessingStatus.translating, + _updateStep( + 'translatingImage'.tr, // 翻译图片 + 'callingImageTranslateService'.tr, // 正在调用图片翻译服务... + ProcessingStatus.translating, progress: 0.6); await _performImageTranslation(file); // 保存历史记录 if (sourceText.value.isNotEmpty && translatedText.value.isNotEmpty) { - _updateStep('保存记录', '正在保存翻译结果...', ProcessingStatus.saving, + _updateStep( + 'savingRecord'.tr, // 保存记录 + 'savingTranslationResult'.tr, // 正在保存翻译结果... + ProcessingStatus.saving, progress: 0.9); await _saveTranslationResult(file); } - _updateStep('翻译完成', '所有步骤已完成', ProcessingStatus.idle, progress: 1.0); + _updateStep( + 'translationCompleted'.tr, // 翻译完成 + 'allStepsCompleted'.tr, // 所有步骤已完成 + ProcessingStatus.idle, + progress: 1.0); } catch (e) { Logger.error('PictureTranslation', '处理失败: $e'); - _updateStep('处理失败', e.toString(), ProcessingStatus.error); - _showErrorDialog('处理失败', '图片处理或翻译失败,请重试。\n错误信息: $e'); + _updateStep( + 'processingFailed'.tr, // 处理失败 + e.toString(), + ProcessingStatus.error); + _showErrorDialog( + 'processingFailed'.tr, // 处理失败 + 'imageProcessingOrTranslationFailed'.tr.replaceAll( + '{error}', e.toString())); // 图片处理或翻译失败,请重试。错误信息: {error} } finally { isProcessing.value = false; } @@ -286,8 +311,13 @@ class PictureTranslationController extends GetxController { // 图片大小限制:4MB(文档要求) const int maxSize = 4 * 1024 * 1024; if (bytes.lengthInBytes > maxSize) { - _updateStep('处理失败', '图片大小超过4MB限制', ProcessingStatus.error); - _showErrorDialog('处理失败', '图片大小超过4MB限制,请降低质量或尺寸后重试'); + _updateStep( + 'processingFailed'.tr, // 处理失败 + 'imageTooLarge'.tr, // 图片大小超过4MB限制 + ProcessingStatus.error); + _showErrorDialog( + 'processingFailed'.tr, // 处理失败 + 'imageTooLargeHint'.tr); // 图片大小超过4MB限制,请降低质量或尺寸后重试 return; } @@ -301,8 +331,13 @@ class PictureTranslationController extends GetxController { ); if (result == null) { - _updateStep('处理失败', '图片翻译接口返回空结果', ProcessingStatus.error); - _showErrorDialog('处理失败', '图片翻译失败,可能的原因:网络异常或接口限流,请稍后重试'); + _updateStep( + 'processingFailed'.tr, // 处理失败 + 'emptyTranslationResult'.tr, // 图片翻译接口返回空结果 + ProcessingStatus.error); + _showErrorDialog( + 'processingFailed'.tr, // 处理失败 + 'imageTranslationFailedTryLater'.tr); // 失败原因提示 return; } @@ -322,8 +357,13 @@ class PictureTranslationController extends GetxController { } } catch (e) { Logger.error('PictureTranslation', '图片翻译调用失败: $e'); - _updateStep('处理失败', e.toString(), ProcessingStatus.error); - _showErrorDialog('处理失败', '图片翻译接口调用异常,请检查网络后重试'); + _updateStep( + 'processingFailed'.tr, // 处理失败 + e.toString(), + ProcessingStatus.error); + _showErrorDialog( + 'processingFailed'.tr, // 处理失败 + 'imageTranslationApiException'.tr); // 接口调用异常 } } @@ -362,7 +402,7 @@ class PictureTranslationController extends GetxController { actions: [ TextButton( onPressed: () => Get.back(), - child: const Text('确定'), + child: Text('confirm'.tr), // 确认 ), ], ), @@ -403,7 +443,9 @@ class PictureTranslationController extends GetxController { void copyText(String text) { if (text.isNotEmpty) { // 这里可以添加复制到剪切板的逻辑 - Get.snackbar('成功', '文本已复制到剪切板'); + Get.snackbar( + 'success'.tr, // 成功 + 'copied'.tr); // 已复制 } } @@ -413,6 +455,9 @@ class PictureTranslationController extends GetxController { translatedText.value = ''; translatedImageBytes.value = null; pickedImageFile.value = null; - _updateStep('等待操作', '请选择图片开始翻译', ProcessingStatus.idle); + _updateStep( + 'waitingForAction'.tr, // 等待操作 + 'pleaseSelectImageToStart'.tr, // 请选择图片开始翻译 + ProcessingStatus.idle); } } diff --git a/lib/modules/picture_translation/views/picture_translation_history_detail_view.dart b/lib/modules/picture_translation/views/picture_translation_history_detail_view.dart index b2fb50c8a..bf664acbc 100644 --- a/lib/modules/picture_translation/views/picture_translation_history_detail_view.dart +++ b/lib/modules/picture_translation/views/picture_translation_history_detail_view.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../core/theme/app_colors.dart'; @@ -20,8 +21,7 @@ class PictureTranslationHistoryDetailView extends StatelessWidget { Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Scaffold( - backgroundColor: - isDarkMode ? AppColors.darkBackground : Colors.grey[50], + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], appBar: AppBar( leading: IconButton( icon: Icon( @@ -32,7 +32,7 @@ class PictureTranslationHistoryDetailView extends StatelessWidget { onPressed: () => Get.back(), ), centerTitle: true, - title: Text('翻译详情', + title: Text('translationDetails'.tr, // 翻译详情 style: TextStyle( color: isDarkMode ? AppColors.textPrimary : Colors.black87)), backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, @@ -56,9 +56,15 @@ class PictureTranslationHistoryDetailView extends StatelessWidget { SizedBox(height: 16.h), _buildLanguageInfo(), SizedBox(height: 16.h), - _buildTextSection('识别结果', item.sourceText, Colors.blue), + _buildTextSection( + 'recognitionResult'.tr, // 识别结果 + item.sourceText, + Colors.blue), SizedBox(height: 16.h), - _buildTextSection('翻译结果', item.translatedText, Colors.green), + _buildTextSection( + 'translationResult'.tr, // 翻译结果 + item.translatedText, + Colors.green), SizedBox(height: 16.h), _buildTimeInfo(), ], @@ -89,12 +95,11 @@ class PictureTranslationHistoryDetailView extends StatelessWidget { Padding( padding: EdgeInsets.all(16.w), child: Text( - '原图片', + 'originalImage'.tr, // 原图片 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, - color: - Get.isDarkMode ? AppColors.textPrimary : Colors.black87, + color: Get.isDarkMode ? AppColors.textPrimary : Colors.black87, ), ), ), @@ -122,7 +127,7 @@ class PictureTranslationHistoryDetailView extends StatelessWidget { size: 48.sp, color: AppColors.error), SizedBox(height: 8.h), Text( - '图片加载失败', + 'imageLoadFailed'.tr, // 图片加载失败 style: TextStyle( fontSize: 14.sp, color: AppColors.error), ), @@ -161,7 +166,7 @@ class PictureTranslationHistoryDetailView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '源语言', + 'sourceLanguage'.tr, // 源语言 style: TextStyle( fontSize: 12.sp, color: Get.isDarkMode @@ -170,7 +175,7 @@ class PictureTranslationHistoryDetailView extends StatelessWidget { ), SizedBox(height: 4.h), Text( - item.sourceLanguageName, + item.sourceLanguageName.tr, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -187,7 +192,7 @@ class PictureTranslationHistoryDetailView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - '目标语言', + 'targetLanguage'.tr, // 目标语言 style: TextStyle( fontSize: 12.sp, color: Get.isDarkMode @@ -196,7 +201,7 @@ class PictureTranslationHistoryDetailView extends StatelessWidget { ), SizedBox(height: 4.h), Text( - item.targetLanguageName, + item.targetLanguageName.tr, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -245,11 +250,13 @@ class PictureTranslationHistoryDetailView extends StatelessWidget { icon: Icon(Icons.copy, size: 20.sp, color: color), onPressed: () { Clipboard.setData(ClipboardData(text: content)); - Get.snackbar('成功', '已复制到剪切板'); + Get.snackbar( + 'success'.tr, // 成功 + 'copied'.tr); // 已复制 }, padding: EdgeInsets.zero, constraints: const BoxConstraints(), - tooltip: '复制', + tooltip: 'copy'.tr, // 复制 ), ], ), @@ -287,12 +294,11 @@ class PictureTranslationHistoryDetailView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '翻译时间', + 'translationTime'.tr, // 翻译时间 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, - color: - Get.isDarkMode ? AppColors.textPrimary : Colors.black87, + color: Get.isDarkMode ? AppColors.textPrimary : Colors.black87, ), ), SizedBox(height: 8.h), @@ -300,7 +306,8 @@ class PictureTranslationHistoryDetailView extends StatelessWidget { _formatDateTime(item.timestamp), style: TextStyle( fontSize: 14.sp, - color: Get.isDarkMode ? AppColors.textSecondary : Colors.grey[600], + color: + Get.isDarkMode ? AppColors.textSecondary : Colors.grey[600], ), ), ], @@ -309,17 +316,18 @@ class PictureTranslationHistoryDetailView extends StatelessWidget { } String _formatDateTime(DateTime dateTime) { - return '${dateTime.year}年${dateTime.month}月${dateTime.day}日 ' - '${dateTime.hour.toString().padLeft(2, '0')}:' - '${dateTime.minute.toString().padLeft(2, '0')}'; + return DateFormat('yyyy-MM-dd HH:mm').format(dateTime); } void _shareTranslation() { - final shareText = '原文:${item.sourceText}\n\n' - '译文:${item.translatedText}\n\n' - '翻译方向:${item.sourceLanguageName} → ${item.targetLanguageName}'; + final shareText = '${'originalText'.tr}:${item.sourceText}\n\n' // 原文 + '${'translatedText'.tr}:${item.translatedText}\n\n' // 译文 + '${'sourceLanguage'.tr}:${item.sourceLanguageName.tr} → ' // 源语言 + '${'targetLanguage'.tr}:${item.targetLanguageName.tr}'; // 目标语言 // 这里可以调用分享插件 - Get.snackbar('提示', '分享功能开发中'); + Get.snackbar( + 'tip'.tr, // 提示 + 'shareInDevelopment'.tr); // 分享功能开发中 } } diff --git a/lib/modules/picture_translation/views/picture_translation_view.dart b/lib/modules/picture_translation/views/picture_translation_view.dart index 491204cc0..b53acb156 100644 --- a/lib/modules/picture_translation/views/picture_translation_view.dart +++ b/lib/modules/picture_translation/views/picture_translation_view.dart @@ -21,8 +21,7 @@ class PictureTranslationView extends GetView { Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Scaffold( - backgroundColor: - isDarkMode ? AppColors.darkBackground : Colors.grey[50], + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], appBar: _buildAppBar(isDarkMode), body: SafeArea( child: Obx(() { @@ -85,7 +84,7 @@ class PictureTranslationView extends GetView { IconButton( icon: const Icon(Icons.history), onPressed: _showHistoryBottomSheet, - tooltip: 'historyRecords'.tr, //历史记录 + tooltip: 'historyRecords'.tr, // 历史记录 ), PopupMenuButton( onSelected: _handleMenuAction, @@ -157,12 +156,11 @@ class PictureTranslationView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '选择图片来源'.tr, //选择图片来源 + 'selectImageSource'.tr, // 选择图片来源 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, - color: - Get.isDarkMode ? AppColors.textPrimary : Colors.black87, + color: Get.isDarkMode ? AppColors.textPrimary : Colors.black87, ), ), SizedBox(height: 12.h), @@ -277,12 +275,11 @@ class PictureTranslationView extends GetView { Padding( padding: EdgeInsets.all(16.w), child: Text( - '图片预览', + 'imagePreview'.tr, // 图片预览 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, - color: - Get.isDarkMode ? AppColors.textPrimary : Colors.black87, + color: Get.isDarkMode ? AppColors.textPrimary : Colors.black87, ), ), ), @@ -377,8 +374,7 @@ class PictureTranslationView extends GetView { ), ); case ProcessingStatus.error: - return Icon(Icons.error_outline, - color: AppColors.error, size: 20.sp); + return Icon(Icons.error_outline, color: AppColors.error, size: 20.sp); case ProcessingStatus.idle: return Icon(Icons.check_circle_outline, color: AppColors.success, size: 20.sp); @@ -423,25 +419,24 @@ class PictureTranslationView extends GetView { Padding( padding: EdgeInsets.all(16.w), child: Text( - '翻译结果', + 'translationResult'.tr, // 翻译结果 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, - color: - Get.isDarkMode ? AppColors.textPrimary : Colors.black87, + color: Get.isDarkMode ? AppColors.textPrimary : Colors.black87, ), ), ), if (controller.sourceText.value.isNotEmpty) _buildTextCard( - '识别结果', + 'recognitionResult'.tr, // 识别结果 controller.sourceText.value, controller.sourceLanguageChineseName.value, Colors.blue, ), if (controller.translatedText.value.isNotEmpty) _buildTextCard( - '翻译结果', + 'translationResult'.tr, // 翻译结果 controller.translatedText.value, controller.targetLanguageChineseName.value, Colors.green, @@ -449,22 +444,22 @@ class PictureTranslationView extends GetView { if (controller.translatedImageBytes.value != null) ...[ Padding( padding: EdgeInsets.symmetric(horizontal: 16.w), - child: Text( - '翻译后的图片预览', - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w600, - color: - Get.isDarkMode ? AppColors.textPrimary : Colors.black87, + child: Text( + 'translatedImagePreview'.tr, // 翻译后的图片预览 + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: + Get.isDarkMode ? AppColors.textPrimary : Colors.black87, + ), ), ), - ), - SizedBox(height: 8.h), - Container( - height: 200.h, - margin: - EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h), - decoration: BoxDecoration( + SizedBox(height: 8.h), + Container( + height: 200.h, + margin: + EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h), + decoration: BoxDecoration( color: Get.isDarkMode ? AppColors.cardBackground : Colors.grey[100], @@ -491,7 +486,7 @@ class PictureTranslationView extends GetView { size: 48.sp, color: Colors.red[400]), SizedBox(height: 8.h), Text( - '翻译图片加载失败', + 'translatedImageLoadFailed'.tr, // 翻译图片加载失败 style: TextStyle( fontSize: 14.sp, color: Colors.red[600]), ), @@ -517,8 +512,8 @@ class PictureTranslationView extends GetView { decoration: BoxDecoration( color: color.withOpacity(Get.isDarkMode ? 0.15 : 0.05), borderRadius: BorderRadius.circular(8.r), - border: Border.all( - color: color.withOpacity(Get.isDarkMode ? 0.3 : 0.2)), + border: + Border.all(color: color.withOpacity(Get.isDarkMode ? 0.3 : 0.2)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -538,11 +533,11 @@ class PictureTranslationView extends GetView { icon: Icon(Icons.copy, size: 18.sp, color: color), onPressed: () { Clipboard.setData(ClipboardData(text: content)); - Get.snackbar('success'.tr, 'copied'.tr); // 翻译键代表的中文文本内容 + Get.snackbar('success'.tr, 'copied'.tr); // 对应中文:复制成功 }, padding: EdgeInsets.zero, constraints: const BoxConstraints(), - tooltip: 'copy'.tr, // 翻译键代表的中文文本内容 + tooltip: 'copy'.tr, // 复制 ), ], ), @@ -551,8 +546,7 @@ class PictureTranslationView extends GetView { content, style: TextStyle( fontSize: 15.sp, - color: - Get.isDarkMode ? Colors.white70 : Colors.black87, + color: Get.isDarkMode ? Colors.white70 : Colors.black87, height: 1.4), ), ], @@ -584,7 +578,7 @@ class PictureTranslationView extends GetView { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'translationHistory'.tr, // 翻译键代表的中文文本内容 + 'translationHistory'.tr, // 翻译历史 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -594,7 +588,7 @@ class PictureTranslationView extends GetView { ), TextButton( onPressed: _showHistoryBottomSheet, - child: Text('viewAll'.tr), // 翻译键代表的中文文本内容 + child: Text('viewAll'.tr), // 查看全部 ), ], ), @@ -604,7 +598,7 @@ class PictureTranslationView extends GetView { padding: EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h), child: Text( - 'noTranslationHistory'.tr, // 翻译键代表的中文文本内容 + 'noTranslationHistory'.tr, // 暂无历史记录 style: TextStyle( fontSize: 14.sp, color: Get.isDarkMode @@ -629,8 +623,9 @@ class PictureTranslationView extends GetView { child: Container( width: 40.w, height: 40.w, - color: - Get.isDarkMode ? Colors.white.withOpacity(0.05) : Colors.grey[200], + color: Get.isDarkMode + ? Colors.white.withOpacity(0.05) + : Colors.grey[200], child: Icon(Icons.image, color: Get.isDarkMode ? Colors.white70 : Colors.grey[400]), ), @@ -642,18 +637,16 @@ class PictureTranslationView extends GetView { style: TextStyle(fontSize: 14.sp), ), subtitle: Text( - '${item.sourceLanguageName} → ${item.targetLanguageName}', + '${item.sourceLanguageName.tr} → ${item.targetLanguageName.tr}', // 源语言名/目标语言名 style: TextStyle( fontSize: 12.sp, - color: - Get.isDarkMode ? AppColors.textSecondary : Colors.grey[600]), + color: Get.isDarkMode ? AppColors.textSecondary : Colors.grey[600]), ), trailing: Text( _formatTime(item.timestamp), style: TextStyle( fontSize: 12.sp, - color: - Get.isDarkMode ? Colors.white54 : Colors.grey[500]), + color: Get.isDarkMode ? Colors.white54 : Colors.grey[500]), ), onTap: () => _showHistoryDetail(item), ); @@ -675,19 +668,16 @@ class PictureTranslationView extends GetView { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'translationHistory'.tr, // 翻译键代表的中文文本内容 + 'translationHistory'.tr, // 翻译历史 style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, - color: Get.isDarkMode - ? AppColors.textPrimary - : null), + color: Get.isDarkMode ? AppColors.textPrimary : null), ), IconButton( onPressed: () => Get.back(), icon: Icon(Icons.close, - color: - Get.isDarkMode ? AppColors.textPrimary : null), + color: Get.isDarkMode ? AppColors.textPrimary : null), ), ], ), @@ -698,7 +688,7 @@ class PictureTranslationView extends GetView { if (controller.historyList.isEmpty) { return Center( child: Text( - 'noTranslationHistory'.tr, // 翻译键代表的中文文本内容 + 'noTranslationHistory'.tr, // 暂无历史记录 style: TextStyle( fontSize: 16.sp, color: Get.isDarkMode @@ -745,13 +735,18 @@ class PictureTranslationView extends GetView { final difference = now.difference(timestamp); if (difference.inDays > 0) { - return '${difference.inDays}天前'; + return 'daysAgo' + .tr + .replaceAll('@days', difference.inDays.toString()); // @days天前 } else if (difference.inHours > 0) { - return '${difference.inHours}小时前'; + return 'hoursAgo' + .tr + .replaceAll('@hours', difference.inHours.toString()); // @hours小时前 } else if (difference.inMinutes > 0) { - return '${difference.inMinutes}分钟前'; + return 'minutesAgo'.tr.replaceAll( + '@minutes', difference.inMinutes.toString()); // @minutes分钟前 } else { - return '刚刚'; + return 'justNow'.tr; // 刚刚 } } @@ -759,7 +754,7 @@ class PictureTranslationView extends GetView { Get.dialog( AlertDialog( backgroundColor: Get.isDarkMode ? Colors.grey[900] : Colors.white, - title: Text('设置', + title: Text('settings'.tr, // 设置 style: TextStyle( color: Get.isDarkMode ? Colors.white : Colors.black87)), content: Column( @@ -768,16 +763,17 @@ class PictureTranslationView extends GetView { ListTile( leading: Icon(Icons.photo_size_select_actual, color: Get.isDarkMode ? Colors.white : null), - title: Text('图片质量', + title: Text('imageQuality'.tr, // 图片质量 + style: TextStyle( + color: Get.isDarkMode ? Colors.white : Colors.black87)), + subtitle: Obx(() => Text( + 'currentQuality'.tr.replaceAll( + '@value', + controller.imageQuality.value + .toString()), // 当前质量: @value% style: TextStyle( color: - Get.isDarkMode ? Colors.white : Colors.black87)), - subtitle: - Obx(() => Text('当前: ${controller.imageQuality.value}%', - style: TextStyle( - color: Get.isDarkMode - ? Colors.white70 - : Colors.black54))), + Get.isDarkMode ? Colors.white70 : Colors.black54))), onTap: () { Get.back(); _showImageQualityDialog(); @@ -786,16 +782,15 @@ class PictureTranslationView extends GetView { ListTile( leading: Icon(Icons.photo_size_select_large, color: Get.isDarkMode ? Colors.white : null), - title: Text('最大尺寸', + title: Text('maxSize'.tr, // 最大尺寸 + style: TextStyle( + color: Get.isDarkMode ? Colors.white : Colors.black87)), + subtitle: Obx(() => Text( + 'currentMaxSize'.tr.replaceAll('@px', + controller.maxImageSize.value.toString()), // 当前: @pxpx style: TextStyle( color: - Get.isDarkMode ? Colors.white : Colors.black87)), - subtitle: - Obx(() => Text('当前: ${controller.maxImageSize.value}px', - style: TextStyle( - color: Get.isDarkMode - ? Colors.white70 - : Colors.black54))), + Get.isDarkMode ? Colors.white70 : Colors.black54))), onTap: () { Get.back(); _showImageSizeDialog(); @@ -806,7 +801,7 @@ class PictureTranslationView extends GetView { actions: [ TextButton( onPressed: () => Get.back(), - child: Text('关闭', + child: Text('close'.tr, // 关闭 style: TextStyle( color: Get.isDarkMode ? Colors.white70 : Colors.black54)), ), @@ -819,13 +814,17 @@ class PictureTranslationView extends GetView { Get.dialog( AlertDialog( backgroundColor: Get.isDarkMode ? Colors.grey[900] : Colors.white, - title: Text('设置图片质量', + title: Text('setImageQuality'.tr, // 设置图片质量 style: TextStyle( color: Get.isDarkMode ? Colors.white : Colors.black87)), content: Obx(() => Column( mainAxisSize: MainAxisSize.min, children: [ - Text('当前质量: ${controller.imageQuality.value}%', + Text( + 'currentQuality'.tr.replaceAll( + '@value', + controller.imageQuality.value + .toString()), // 当前质量: @value% style: TextStyle( color: Get.isDarkMode ? Colors.white70 : Colors.black54)), @@ -843,7 +842,7 @@ class PictureTranslationView extends GetView { actions: [ TextButton( onPressed: () => Get.back(), - child: Text('确定', + child: Text('confirm'.tr, // 确认 style: TextStyle( color: Get.isDarkMode ? Colors.white70 : Colors.black54)), ), @@ -857,7 +856,7 @@ class PictureTranslationView extends GetView { Get.dialog( AlertDialog( backgroundColor: Get.isDarkMode ? Colors.grey[900] : Colors.white, - title: Text('设置最大尺寸', + title: Text('setMaxSize'.tr, // 设置最大尺寸 style: TextStyle( color: Get.isDarkMode ? Colors.white : Colors.black87)), content: Column( @@ -879,7 +878,7 @@ class PictureTranslationView extends GetView { actions: [ TextButton( onPressed: () => Get.back(), - child: Text('取消', + child: Text('cancel'.tr, // 取消 style: TextStyle( color: Get.isDarkMode ? Colors.white70 : Colors.black54)), ), @@ -892,16 +891,16 @@ class PictureTranslationView extends GetView { Get.dialog( AlertDialog( backgroundColor: Get.isDarkMode ? Colors.grey[900] : Colors.white, - title: Text('清空历史', + title: Text('clearHistory'.tr, // 清除历史记录 style: TextStyle( color: Get.isDarkMode ? Colors.white : Colors.black87)), - content: Text('确定要清空所有翻译历史吗?此操作不可撤销。', + content: Text('clearHistoryConfirm'.tr, // 确定要清除所有翻译历史记录吗?此操作无法撤销。 style: TextStyle( color: Get.isDarkMode ? Colors.white70 : Colors.black54)), actions: [ TextButton( onPressed: () => Get.back(), - child: Text('取消', + child: Text('cancel'.tr, // 取消 style: TextStyle( color: Get.isDarkMode ? Colors.white70 : Colors.black54)), ), @@ -910,7 +909,8 @@ class PictureTranslationView extends GetView { controller.clearHistory(); Get.back(); }, - child: Text('确定', style: TextStyle(color: AppColors.error)), + child: Text('confirm'.tr, // 确认 + style: TextStyle(color: AppColors.error)), ), ], ), @@ -923,11 +923,10 @@ class PictureTranslationView extends GetView { height: 200.h, margin: EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h), decoration: BoxDecoration( - color: - Get.isDarkMode ? AppColors.cardBackground : Colors.grey[100], + color: Get.isDarkMode ? AppColors.cardBackground : Colors.grey[100], borderRadius: BorderRadius.circular(8.r), - border: - Border.all(color: Get.isDarkMode ? Colors.white12 : Colors.grey[300]!), + border: Border.all( + color: Get.isDarkMode ? Colors.white12 : Colors.grey[300]!), ), child: file == null ? Center( @@ -936,10 +935,11 @@ class PictureTranslationView extends GetView { children: [ Icon(Icons.image_outlined, size: 48.sp, - color: Get.isDarkMode ? Colors.white54 : Colors.grey[400]), + color: + Get.isDarkMode ? Colors.white54 : Colors.grey[400]), SizedBox(height: 8.h), Text( - '请选择图片', + 'pleaseSelectImage'.tr, // 请选择图片 style: TextStyle( fontSize: 14.sp, color: Get.isDarkMode @@ -967,7 +967,7 @@ class PictureTranslationView extends GetView { size: 48.sp, color: Colors.red[400]), SizedBox(height: 8.h), Text( - '图片加载失败', + 'imageLoadFailed'.tr, // 图片加载失败 style: TextStyle( fontSize: 14.sp, color: Get.isDarkMode @@ -985,11 +985,15 @@ class PictureTranslationView extends GetView { } Widget _buildSourceTextBox() { - return _buildTextBox('识别结果', controller.sourceText.value); + return _buildTextBox( + 'recognitionResult'.tr, // 识别结果 + controller.sourceText.value); } Widget _buildTranslatedTextBox() { - return _buildTextBox('翻译结果', controller.translatedText.value); + return _buildTextBox( + 'translationResult'.tr, // 翻译结果 + controller.translatedText.value); } Widget _buildTextBox(String title, String content) { diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart index 618adae96..4eff2d73e 100644 --- a/lib/modules/settings/controllers/settings_controller.dart +++ b/lib/modules/settings/controllers/settings_controller.dart @@ -10,6 +10,7 @@ import 'package:package_info_plus/package_info_plus.dart'; import '../../../core/themes/theme_service.dart'; import '../../../core/translations/app_translations.dart'; +import '../../../data/services/network/dio_manager.dart'; import '../../../data/services/speech_factory.dart'; import '../../../routes/app_routes.dart'; import '../models/language_model.dart'; @@ -651,7 +652,7 @@ class SettingsController extends GetxController { 最近版本检查时间: ${versionService.lastCheckTime ?? '未检查'} 国家地区编码: ${versionService.cachedCountryCode} 语言编码: ${versionService.cachedLanguageCode} -服务器地址: ${versionService.cachedServerUrl} +服务器地址: ${DioManager().baseUrl} '''; } catch (e) { return '获取版本服务数据失败: $e'; diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index d9e944d33..d42906ba3 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -40,9 +40,11 @@ class SettingsView extends GetView { padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), children: [ _buildUserInfoCard(isDarkMode), + SizedBox(height: 12.h), + _buildVipPurchaseCard(isDarkMode), // SizedBox(height: 20.h), // _buildAISettingsSection(isDarkMode), - SizedBox(height: 20.h), + SizedBox(height: 16.h), _buildSystemSettingsSection(isDarkMode), SizedBox(height: 20.h), _buildAboutSection(isDarkMode), @@ -127,34 +129,34 @@ class SettingsView extends GetView { ), overflow: TextOverflow.ellipsis, ), - SizedBox(height: 4.h), - Row( + // SizedBox(height: 4.h), + const Row( children: [ - Container( - padding: EdgeInsets.symmetric( - horizontal: 8.w, vertical: 2.h), - decoration: BoxDecoration( - color: Colors.blue[600], - borderRadius: BorderRadius.circular(10.r), - ), - child: Obx(() { - final membershipMap = { - 0: 'freeUser', //'免费用户' - 1: 'standardUser', //'标准用户' - 2: 'proUser', //'专业用户' - }; - final membershipString = membershipMap[ - profileCtrl.userInfo.value.viplv] ?? - 'Unknown'; - return Text( - membershipString.tr, // 会员等级 - style: TextStyle( - fontSize: 10.sp, - color: Colors.white, - ), - ); - }), - ), + // Container( + // padding: EdgeInsets.symmetric( + // horizontal: 8.w, vertical: 2.h), + // decoration: BoxDecoration( + // color: Colors.blue[600], + // borderRadius: BorderRadius.circular(10.r), + // ), + // child: Obx(() { + // final membershipMap = { + // 0: 'freeUser', //'免费用户' + // 1: 'standardUser', //'标准用户' + // 2: 'proUser', //'专业用户' + // }; + // final membershipString = membershipMap[ + // profileCtrl.userInfo.value.viplv] ?? + // 'Unknown'; + // return Text( + // membershipString.tr, // 会员等级 + // style: TextStyle( + // fontSize: 10.sp, + // color: Colors.white, + // ), + // ); + // }), + // ), // SizedBox(width: 8.w), // Flexible( // child: Text( @@ -215,6 +217,146 @@ class SettingsView extends GetView { ); } + // 构建Vip购买卡片 + Widget _buildVipPurchaseCard(bool isDarkMode) { + return Container( + padding: EdgeInsets.fromLTRB(16.w, 16.h, 16.w, 12.h), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: isDarkMode + ? [ + const Color(0xFF0D47A1).withOpacity(0.5), + const Color(0xFF1976D2).withOpacity(0.5) + ] + : [const Color(0xFFE3F2FD), const Color(0xFFBBDEFB)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'vipRecharge'.tr, // 会员充值 + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + SizedBox(height: 12.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildCategoryItem( + icon: Icons.workspace_premium, + label: 'aiRecharge'.tr, //AI充值 + iconBg: isDarkMode + ? Colors.amber[900]!.withOpacity(0.3) + : Colors.amber[100]!, + iconColor: isDarkMode ? Colors.amber[300]! : Colors.amber[600]!, + textColor: isDarkMode ? Colors.white : Colors.black87, + onTap: () => Get.toNamed(Routes.goodsvip), + ), + _buildCategoryItem( + icon: Icons.translate, + label: 'translationRecharge'.tr, //翻译充值 + iconBg: isDarkMode + ? Colors.indigo[900]!.withOpacity(0.3) + : Colors.indigo[100]!, + iconColor: + isDarkMode ? Colors.indigo[300]! : Colors.indigo[600]!, + textColor: isDarkMode ? Colors.white : Colors.black87, + onTap: () => Get.toNamed(Routes.goodstrans), + ), + _buildCategoryItem( + icon: Icons.people, + label: 'meetingAssistantRecharge'.tr, //会议充值 + iconBg: isDarkMode + ? Colors.pink[900]!.withOpacity(0.3) + : Colors.pink[100]!, + iconColor: isDarkMode ? Colors.pink[300]! : Colors.pink[600]!, + textColor: isDarkMode ? Colors.white : Colors.black87, + onTap: () => Get.toNamed(Routes.goodsmeet), + ), + _buildCategoryItem( + icon: Icons.analytics_outlined, + label: 'usageStats'.tr, //用量统计 + iconBg: isDarkMode + ? Colors.teal[900]!.withOpacity(0.3) + : Colors.teal[100]!, + iconColor: isDarkMode ? Colors.teal[300]! : Colors.teal[600]!, + textColor: isDarkMode ? Colors.white : Colors.black87, + onTap: () => Get.toNamed(Routes.usageStats), + ), + ], + ), + ], + ), + ); + } + + Widget _buildCategoryItem({ + required IconData icon, + required String label, + required Color iconBg, + required Color iconColor, + required VoidCallback onTap, + Color? textColor, + }) { + return InkWell( + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 42.w, + height: 42.w, + decoration: BoxDecoration( + color: iconBg, + borderRadius: BorderRadius.circular(21.r), + ), + child: Icon( + icon, + size: 20.w, + color: iconColor, + ), + ), + SizedBox(height: 5.h), + SizedBox( + width: 72.w, + height: 32.h, + child: Center( + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: textColor ?? + (Get.isDarkMode ? Colors.white : Colors.black87), + height: 1.2, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ); + } + // AI设置 Widget _buildAISettingsSection(bool isDarkMode) { return Column( diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index 60631b889..eabfbbadb 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -10,6 +10,9 @@ import 'package:http/http.dart' as http; import '../../../data/models/appconfig.dart'; import '../../../data/models/appconfig_model.dart'; import '../../../data/models/user_Info.dart'; +import '../../../data/services/meeting/meeting_task_service.dart'; +import '../../../data/services/meeting/meeting_upload_service.dart'; +import '../../../data/services/meeting/summary_task_service.dart'; import '../../../routes/app_routes.dart'; import '../../../data/services/network/api.dart'; import '../../../data/services/version_update_service.dart'; @@ -266,8 +269,16 @@ class SplashController extends GetxController { final isAppConfigInitialized = AppConfig.isInitialized(); // 检查登录状态和配置初始化状态 if (token != null && token.isNotEmpty && isAppConfigInitialized) { + Get.find(); + Get.find(); + Get.find(); Get.offAllNamed(Routes.pairing); } else { + _storage.remove('meeting_task_list'); + _storage.remove('meeting_task_finish_list'); + _storage.remove('meeting_upload_list'); + _storage.remove('operation_records'); + _storage.remove('operation_delids'); Get.offAllNamed(Routes.login); } } diff --git a/lib/modules/text_translation/controllers/text_translation_controller.dart b/lib/modules/text_translation/controllers/text_translation_controller.dart index 8b4b1ed5e..131a560f1 100644 --- a/lib/modules/text_translation/controllers/text_translation_controller.dart +++ b/lib/modules/text_translation/controllers/text_translation_controller.dart @@ -257,8 +257,8 @@ class TextTranslationController extends GetxController { if (detectedAsrCode == null || detectedAsrCode.isEmpty) { Logger.error('语种检测失败'); Get.snackbar( - 'error'.tr, // 翻译键代表的中文文本内容 - 'languageDetectionFailed'.tr, // 翻译键代表的中文文本内容 + 'error'.tr, // 错误 + 'languageDetectionFailed'.tr, // 语言检测失败 snackPosition: SnackPosition.TOP, backgroundColor: Colors.red.withOpacity(0.9), colorText: Colors.white, @@ -288,8 +288,8 @@ class TextTranslationController extends GetxController { } catch (e) { Logger.error('翻译失败: $e'); Get.snackbar( - 'error'.tr, - 'translationFailed'.tr, + 'error'.tr, // 错误 + 'translationFailed'.tr, // 翻译失败 snackPosition: SnackPosition.TOP, backgroundColor: Colors.red.withOpacity(0.9), colorText: Colors.white, diff --git a/lib/modules/text_translation/views/text_translation_view.dart b/lib/modules/text_translation/views/text_translation_view.dart index 723e3cd48..0c7dc317a 100644 --- a/lib/modules/text_translation/views/text_translation_view.dart +++ b/lib/modules/text_translation/views/text_translation_view.dart @@ -99,10 +99,12 @@ class TextTranslationView extends GetView { return LanguageSelectorBar( sourceText: () => controller.sourceLanguageChineseName.value, targetText: () => controller.targetLanguageChineseName.value, - onTapSource: () => controller.showLanguageSelector(isSourceLanguage: true), - onTapTarget: () => controller.showLanguageSelector(isSourceLanguage: false), + onTapSource: () => + controller.showLanguageSelector(isSourceLanguage: true), + onTapTarget: () => + controller.showLanguageSelector(isSourceLanguage: false), onSwap: controller.swapLanguages, - translateLanguageNames: true, // 翻译键代表的中文文本内容 + translateLanguageNames: true, // 是否翻译语言名称 fontSizeSp: 15, ); } diff --git a/lib/modules/timbre/bindings/timbre_binding.dart b/lib/modules/timbre/bindings/timbre_binding.dart index 6883836f7..15a9ecdb4 100644 --- a/lib/modules/timbre/bindings/timbre_binding.dart +++ b/lib/modules/timbre/bindings/timbre_binding.dart @@ -4,6 +4,6 @@ import '../controllers/timbre_controller.dart'; class TimbreBinding extends Bindings { @override void dependencies() { - Get.put(TimbreController()); + Get.lazyPut(() => TimbreController()); } } diff --git a/lib/modules/timbre/controllers/timbre_controller.dart b/lib/modules/timbre/controllers/timbre_controller.dart index d976105fa..c5ebcc4b5 100644 --- a/lib/modules/timbre/controllers/timbre_controller.dart +++ b/lib/modules/timbre/controllers/timbre_controller.dart @@ -9,12 +9,7 @@ import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:record/record.dart'; -import '../../../data/services/speech_factory.dart'; -import '../../settings/controllers/settings_controller.dart'; - class TimbreController extends GetxController { - final SettingsController settingsController = Get.find(); - final GetStorage _storage = GetStorage(); final _record = AudioRecorder(); @@ -153,7 +148,7 @@ class TimbreController extends GetxController { // 创建训练任务 Future _addTask() async { - String data = '{"resourceType":"12"}'; + String data = '{"resourceType":"12","engineVersion":"omni_v1"}'; Map sign = _sign(data); final response = await http.post( @@ -291,7 +286,6 @@ class TimbreController extends GetxController { void selectTone(Map data) { if (!data['isUsable'] || type.value == data['type']) return; type.value = data['type']; - settingsController.timbreName.value = data['name']; _storage.write('timbre', data['type']); } } diff --git a/lib/modules/translation/bindings/translation_stats_binding.dart b/lib/modules/translation/bindings/translation_stats_binding.dart deleted file mode 100644 index 45c2824b1..000000000 --- a/lib/modules/translation/bindings/translation_stats_binding.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/translation_stats_controller.dart'; -import '../../../data/services/translation_usage_service.dart'; - -//翻译统计页面依赖绑定 -class TranslationStatsBinding implements Bindings { - @override - void dependencies() { - // 确保服务已注册 - if (!Get.isRegistered()) { - Get.put(TranslationUsageService(), permanent: true); - } - - // 注册控制器 - Get.lazyPut( - () => TranslationStatsController(), - ); - } -} diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 037ea6aa2..8ceb5a382 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -2,15 +2,19 @@ import 'dart:io'; import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:intl/intl.dart'; +import 'package:native_plugin/native_plugin.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:pip/pip.dart'; import '../../../data/services/ast_service.dart'; import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/music_manager.dart'; +import '../../../data/services/translation_usage_service.dart'; import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/tts_service.dart'; import '../../../data/services/language_manager.dart'; @@ -19,12 +23,14 @@ import '../../../core/utils/permission_util.dart'; import '../../../core/services/floating_window_manager.dart'; import '../../../data/services/asr_service.dart'; -import '../../../data/services/translation_usage_service.dart'; +import '../../../data/services/usage_stats_service.dart'; import '../../../data/services/ble_manager.dart'; import 'record_timer_controller.dart'; import 'translation_history_controller.dart'; import '../models/translation_models.dart'; import '../views/dialogs/translation_mode_selector.dart'; +import '../../../data/models/user_Info.dart'; +import '../../common/widgets/recharge_dialog.dart'; /// 音频输出设备枚举 enum AudioOutputDevice { @@ -48,8 +54,13 @@ class TranslationController extends GetxController with WidgetsBindingObserver { final BluetoothManager bluetoothManager = Get.find(); final BleManager bleManager = Get.find(); final _musiceManager = Get.find(); - late final TranslationUsageService _usageService; + late final UsageStatsService _usageService; + // 安卓悬浮窗管理器 late final FloatingWindowManager _floatingWindowManager; + // 苹果悬浮窗管理器 + late final Pip _pip; + late final NativePlugin _nativePlugin; + int _pipContentView = 0; // ==================== 悬浮窗相关 ==================== final isFloatingWindowEnabled = false.obs; // 悬浮窗状态 @@ -79,6 +90,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { RxInt activeSpeaker = 0.obs; // 0=无激活, 1=按钮1, 2=按钮2 RxInt lastActiveSpeaker = 0.obs; // 上一次状态 final isRecognizing = false.obs; + final isPreparing = false.obs; final isTranslating = false.obs; final isTtsEnabled = true.obs; final isRecording = false.obs; @@ -135,7 +147,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { // 底部控制栏高度 double _bottomBarHeight = 0.0; - /// 初始化悬浮窗管理器 + /// 初始化安卓悬浮窗管理器 void _initFloatingWindowManager() { try { if (!Get.isRegistered()) { @@ -148,15 +160,70 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } + /// 初始化苹果悬浮窗管理器 + Future initPlatformState() async { + _pip = Pip(); + _nativePlugin = NativePlugin(); + try { + isFloatingWindowEnabled.value = await _pip.isActived(); + await _pip.registerStateChangedObserver( + PipStateChangedObserver( + onPipStateChanged: (state, error) { + isFloatingWindowEnabled.value = state == PipState.pipStateStarted; + + if (state == PipState.pipStateFailed) { + _pip.dispose(); + } + }, + ), + ); + } on PlatformException { + isFloatingWindowEnabled.value = false; + } + } + /// 切换悬浮窗功能 Future toggleFloatingWindow() async { if (isFloatingWindowEnabled.value) { - await _disableFloatingWindow(); + if (Platform.isAndroid) { + await _disableFloatingWindow(); + } else if (Platform.isIOS) { + await _pip.dispose(); + } } else { - await _enableFloatingWindow(); + if (Platform.isAndroid) { + await _enableFloatingWindow(); + } else if (Platform.isIOS) { + await _startPip(); + } } } + Future _startPip() async { + if (_pipContentView == 0) { + _pipContentView = await _nativePlugin.createPipContentView(); + } + final options = PipOptions( + autoEnterEnabled: true, + aspectRatioX: 375, + aspectRatioY: 400, + sourceRectHintLeft: 0, + sourceRectHintTop: 0, + sourceRectHintRight: 0, + sourceRectHintBottom: 0, + seamlessResizeEnabled: true, + useExternalStateMonitor: true, + externalStateMonitorInterval: 100, + contentView: _pipContentView, + preferredContentWidth: 375, + preferredContentHeight: 400, + controlStyle: 2, + ); + + await _pip.setup(options); + await _pip.start(); + } + /// 启用悬浮窗功能 Future _enableFloatingWindow() async { // 检查是否在支持悬浮窗的模式 @@ -217,16 +284,24 @@ class TranslationController extends GetxController with WidgetsBindingObserver { _languageManager.getChineseNameByAsrCode(item.targetLanguageCode) ?? 'unknown'; - final data = FloatingWindowData( - sourceText: item.sourceText, - translatedText: item.translatedText, - sourceLanguage: sourceLanguageName, - targetLanguage: targetLanguageName, - timestamp: item.timestamp, - isIntermediate: item.isIntermediate, - ); - - await _floatingWindowManager.updateFloatingWindowContent(data); + if (Platform.isAndroid) { + final data = FloatingWindowData( + sourceText: item.sourceText, + translatedText: item.translatedText, + sourceLanguage: sourceLanguageName, + targetLanguage: targetLanguageName, + timestamp: item.timestamp, + isIntermediate: item.isIntermediate, + ); + await _floatingWindowManager.updateFloatingWindowContent(data); + } else if (Platform.isIOS) { + await _nativePlugin.updatePipContentView({ + 'viewId': _pipContentView, + 'cn': item.sourceText, + 'en': item.translatedText, + 'lang': '$sourceLanguageName -> $targetLanguageName', + }); + } } /// 更改翻译模式 @@ -234,6 +309,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver { Future changeTranslationMode(String mode) async { currentMode.value = mode; currentModeTitle.value = _getModeTitle(mode); + _historyManager.setMode(mode); + loadTranslationHistory(); //stopAll(); // 检查悬浮窗支持情况 @@ -318,7 +395,18 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// [speakerId] 说话者ID Future _startFaceToFaceRecognition(int speakerId) async { try { - await _asrService.startContinuousRecognition(_audioSourceType); //开启识别 + if (User.isLoggedIn() && User.instance.tradeintegral <= 0) { + showRechargeDialog('translation'); + activeSpeaker.value = 0; + return; + } + if (_asrSessionStartTime == null) { + await _initializeSession(); + } + await _asrService.startContinuousRecognition( + _audioSourceType, + isRemoveFirstPunctuation: false, + ); //开启识别 _startAsrActiveTracking(); //开启识别活动跟踪 if (isRecording.value) { if (isCreateRecord.value) { @@ -347,6 +435,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } await _asrService.stopContinuousRecognition(); _stopAsrActiveTracking(); //停止识别活动跟踪 + _recordAsrUsageStats(sourceLanguage.value); isRecognizing.value = false; currentSessionId = null; @@ -372,8 +461,13 @@ class TranslationController extends GetxController with WidgetsBindingObserver { _timerManager = RecordTimerController(); _historyManager = TranslationHistoryManager(); - // 初始化悬浮窗管理器 - _initFloatingWindowManager(); + if (Platform.isAndroid) { + // 初始化安卓悬浮窗管理器 + _initFloatingWindowManager(); + } else if (Platform.isIOS) { + // 初始化苹果悬浮窗管理器 + initPlatformState(); + } // 预先检查权限,避免在用户交互时阻塞 _preCheckPermissions(); @@ -397,7 +491,11 @@ class TranslationController extends GetxController with WidgetsBindingObserver { targetLanguageCode.value = targetAsrCode; } - // 加载历史记录 + // 初始化翻译模式(用于历史隔离的初始加载) + final initMode = Get.arguments?['translationId'] ?? 'simultaneous'; + _historyManager.setMode(initMode); + + // 按初始模式加载历史 loadTranslationHistory(); // 初始化服务 @@ -406,9 +504,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver { // 初始化历史管理器 _historyManager.initialize(); - // 初始化翻译模式,根据跳转参数 - final mode = Get.arguments?['translationId'] ?? 'simultaneous'; - changeTranslationMode(mode); + // 应用模式逻辑(含ASR/TTS配置等) + changeTranslationMode(initMode); // 监听应用生命周期:当应用切到后台/失焦时兜底重置 activeSpeaker WidgetsBinding.instance.addObserver(this); @@ -431,17 +528,17 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - /// 初始化统计服务 + /// 初始化用量统计服务 void _initUsageService() { try { // 确保统计服务已注册 - if (!Get.isRegistered()) { - Get.put(TranslationUsageService(), permanent: true); + if (!Get.isRegistered()) { + Get.put(UsageStatsService(), permanent: true); } - _usageService = Get.find(); - Logger.info('翻译统计服务初始化完成'); + _usageService = Get.find(); + Logger.info('使用量统计服务初始化完成'); } catch (e) { - Logger.error('翻译统计服务初始化失败: $e'); + Logger.error('使用量统计服务初始化失败: $e'); } } @@ -660,6 +757,15 @@ class TranslationController extends GetxController with WidgetsBindingObserver { // _asrService.dispose(); // _astService.dispose(); + if (Platform.isAndroid) { + _disableFloatingWindow(); + } else if (Platform.isIOS) { + _pip.dispose(); + if (_pipContentView != 0) { + _nativePlugin.disposePipContentView(_pipContentView); + } + } + // 释放管理器资源 _timerManager.dispose(); _historyManager.dispose(); @@ -746,6 +852,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// 开始语音识别 Future startRecognition() async { Logger.info('开始语音识别'); + if (User.isLoggedIn() && User.instance.tradeintegral <= 0) { + showRechargeDialog('translation'); + return; + } if (!await _checkPermissions()) return; if (isRecognizing.value) return; @@ -754,10 +864,16 @@ class TranslationController extends GetxController with WidgetsBindingObserver { await _initializeSession(); await _configureAudioMode(); if (currentMode.value != 'faceToFace') { + if ((currentMode.value == 'audioVideo' || + currentMode.value == 'call') && + !bleManager.isCodecActive) { + return; + } await _startAsrService(); await _finalizeRecognitionStart(); } } catch (e) { + isPreparing.value = false; isRecognizing.value = false; Logger.error('启动语音识别失败: ${e.toString()}'); } @@ -803,7 +919,18 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// 配置音视频模式 Future _configureAudioVideoMode() async { - bleManager.openDecoder(); + isPreparing.value = true; + await bleManager.openDecoder(); + int attempts = 0; + while (!bleManager.isCodecActive && attempts < 10) { + await Future.delayed(const Duration(milliseconds: 150)); + attempts++; + } + isPreparing.value = false; + if (!bleManager.isCodecActive) { + Get.snackbar('tip'.tr, 'operationFailedPleaseRetry'.tr); + throw Exception('lxm--BLE编解码打开失败'); + } _audioSourceType = true; isTtsEnabled.value = false; Logger.info('发送ble音乐或者通话远端声音'); @@ -817,7 +944,18 @@ class TranslationController extends GetxController with WidgetsBindingObserver { ]; Logger.info('1初始化AST服务,支持语言: $callModeLanguages'); //ast=asr+translate await _astService.initialize(supportedLanguages: callModeLanguages); - bleManager.openA2DPDecoder(); + isPreparing.value = true; + await bleManager.openA2DPDecoder(); + int attempts = 0; + while (!bleManager.isCodecActive && attempts < 10) { + await Future.delayed(const Duration(milliseconds: 150)); + attempts++; + } + isPreparing.value = false; + if (!bleManager.isCodecActive) { + Get.snackbar('tip'.tr, 'bluetoothCallStateError'.tr); + throw Exception('lxm--BLE编解码打开失败'); + } _audioSourceType = true; isTtsEnabled.value = false; Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音'); @@ -847,53 +985,64 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// 处理语音识别事件 /// [event] 识别事件 void _handleRecognitionEvent(RecognitionEvent event) { - if (event.type == RecognitionEventType.finalResult && - event.text.isNotEmpty) { - detectedLanguageCode = event.detectedLanguage; - print("处理识别完成:${event.text}"); - if (currentMode.value != 'faceToFace') { - handleFinalResult(event.text); - } else { - faceToFaceRecognitionText.value += event.text; - // 添加日志打印 - Logger.info('lxm-面对面翻译识别文本更新: ${faceToFaceRecognitionText.value}'); - } - if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) { - handleFinalResult(faceToFaceRecognitionText.value); - faceToFaceRecognitionText.value = ''; - } - } else if (event.type == RecognitionEventType.finalResult1 && - event.text.isNotEmpty) { - // 通话模式下:己方(麦克风)识别完成事件 - // 若原生未返回检测语言,则兜底为源语言 - detectedLanguageCode = event.detectedLanguage.isNotEmpty - ? event.detectedLanguage - : sourceLanguageCode.value; - print("处理己方识别完成:${event.text}"); - if (currentMode.value != 'faceToFace') { + // Logger.info("处理识别事件:${event}"); + switch (event.type) { + case RecognitionEventType.finalResult: + if (event.text.isEmpty) break; + // 通话模式下:目标语言即为识别语言,无需判断 + detectedLanguageCode = currentMode.value == 'call' + ? targetLanguageCode.value + : event.detectedLanguage; + Logger.info("处理识别完成:${event.text}"); + if (currentMode.value != 'faceToFace') { + handleFinalResult(event.text); + } else { + faceToFaceRecognitionText.value += event.text; + Logger.info('lxm-面对面翻译识别文本更新: ${faceToFaceRecognitionText.value}'); + } + if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) { + handleFinalResult(faceToFaceRecognitionText.value); + faceToFaceRecognitionText.value = ''; + } + break; + + case RecognitionEventType.finalResult1: + // 通话模式下:己方(麦克风)识别完成事件(对应ast服务) + if (event.text.isEmpty) break; + //通话模式下:己方语言即为源语言,无需判断 + detectedLanguageCode = sourceLanguageCode.value; + Logger.info("lxm---处理己方识别完成:${event.text}"); handleFinalResult(event.text); - } else { - faceToFaceRecognitionText.value += event.text; - Logger.info('lxm-面对面翻译识别文本更新(己方): ${faceToFaceRecognitionText.value}'); - } - if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) { - handleFinalResult(faceToFaceRecognitionText.value); - faceToFaceRecognitionText.value = ''; - } - } else if (event.type == RecognitionEventType.intermediateResult && - event.text.isNotEmpty) { - if (event.detectedLanguage.isNotEmpty) { - detectedLanguageCode = event.detectedLanguage; - } - print("处理识别中事件:${event.text}"); - handleIntermediateResult(event.text); - } else if (event.type == RecognitionEventType.error) { - Get.snackbar('error'.tr, event.error); - isRecognizing.value = false; - Logger.error('语音识别错误: ${event.error}'); - } else if (event.type == RecognitionEventType.canceled) { - isRecognizing.value = false; - Logger.error('语音识别取消: ${event.error}'); + break; + + case RecognitionEventType.intermediateResult: + if (event.text.isEmpty) break; + // 通话模式下:目标语言即为识别语言,无需判断 + detectedLanguageCode = currentMode.value == 'call' + ? targetLanguageCode.value + : event.detectedLanguage; + Logger.info("处理识别中事件:${event.text}"); + handleIntermediateResult(event.text); + break; + + case RecognitionEventType.error: + Get.snackbar('error'.tr, event.error); + isRecognizing.value = false; + Logger.error('语音识别错误: ${event.error}'); + break; + + case RecognitionEventType.canceled: + isRecognizing.value = false; + Logger.error('语音识别取消: ${event.error}'); + break; + + case RecognitionEventType.sessionStarted: + // 可按需处理会话开始事件 + break; + + case RecognitionEventType.sessionStopped: + // 可按需处理会话结束事件 + break; } } @@ -908,6 +1057,12 @@ class TranslationController extends GetxController with WidgetsBindingObserver { await _astService.stopContinuousTranslation(); } _stopAsrActiveTracking(); + final langName = _languageManager.getChineseNameByAsrCode( + (detectedLanguageCode.isNotEmpty + ? detectedLanguageCode + : sourceLanguageCode.value)) ?? + '未知'; + _recordAsrUsageStats(langName); isRecognizing.value = false; currentSessionId = null; } catch (e) { @@ -919,18 +1074,21 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// [text] 识别的文本 void handleIntermediateResult(String text) { if (text.isEmpty) return; - print( + Logger.info( "handleIntermediateResult:${translationHistory.isEmpty}${translationHistory.isNotEmpty ? translationHistory.last.isIntermediate : 'N/A'}"); if (translationHistory.isEmpty || (translationHistory.isNotEmpty && !translationHistory.last.isIntermediate)) { currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString(); + // 关键调整:使用与最终结果一致的语言方向,避免左右“跳变” + final languageCodes = _determineLanguageCodes(); + final newItem = TranslationItem( sourceText: text, translatedText: '', - sourceLanguageCode: sourceLanguageCode.value, - targetLanguageCode: targetLanguageCode.value, + sourceLanguageCode: languageCodes['source']!, + targetLanguageCode: languageCodes['target']!, timestamp: DateTime.now(), sessionId: currentSessionId!, isFirstInSession: translationHistory.isEmpty || @@ -971,10 +1129,11 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// 处理最终识别结果 /// [text] 识别的文本 Future handleFinalResult(String text) async { - if (text.isEmpty || translationHistory.isEmpty) return; + if (text.isEmpty) return; // 更新当前项 - if (translationHistory.last.isIntermediate) { + if (translationHistory.isNotEmpty && + translationHistory.last.isIntermediate) { translationHistory.last.sourceText = text; translationHistory.last.isIntermediate = false; translationHistory.refresh(); @@ -1203,7 +1362,6 @@ class TranslationController extends GetxController with WidgetsBindingObserver { Logger.info( '翻译统计: 源文本${sourceText.length}字符, 模式${currentMode.value}, 语言对$sourceLanguageName→$targetLanguageName'); - _recordAsrUsageStats(sourceLanguageName); } catch (e) { Logger.error('记录翻译统计失败: $e'); } @@ -1275,6 +1433,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// 停止所有操作 Future stopAll() async { try { + isPreparing.value = false; if (currentMode.value == "audioVideo" || currentMode.value == "call") { bleManager.closeCodec(); Logger.info('关闭ble编解码器'); @@ -1592,6 +1751,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver { Logger.info( 'ASR会话统计: 总时长${totalDurationSeconds}秒, 实际活跃时长${effectiveDurationSeconds}秒, 语言$language'); + _asrSessionStartTime = null; + _asrActiveDuration = Duration.zero; + _isAsrActive = false; + _asrActiveStartTime = null; } catch (e) { Logger.error('记录ASR统计失败: $e'); } diff --git a/lib/modules/translation/controllers/translation_cost_controller.dart b/lib/modules/translation/controllers/translation_cost_controller.dart deleted file mode 100644 index 75b7fccff..000000000 --- a/lib/modules/translation/controllers/translation_cost_controller.dart +++ /dev/null @@ -1,258 +0,0 @@ -import 'package:get/get.dart'; -import 'package:flutter/material.dart'; -import '../../../data/services/translation_usage_service.dart'; -import '../../../core/utils/logger.dart'; - -/// 翻译成本配置控制器 -class TranslationCostController extends GetxController { - // 依赖注入 - final TranslationUsageService _usageService = Get.find(); - - // 成本配置 - final RxMap costConfig = {}.obs; - final RxDouble dailyCostThreshold = 10.0.obs; - final RxDouble monthlyCostThreshold = 300.0.obs; - - // 表单控制器 - final translationCostController = TextEditingController(); - final asrCostController = TextEditingController(); - final ttsCostController = TextEditingController(); - final aiSummaryCostController = TextEditingController(); - final dailyThresholdController = TextEditingController(); - final monthlyThresholdController = TextEditingController(); - - // 状态变量 - final isLoading = false.obs; - final isSaving = false.obs; - - @override - void onInit() { - super.onInit(); - _loadCurrentConfig(); - } - - @override - void onClose() { - // 释放控制器 - translationCostController.dispose(); - asrCostController.dispose(); - ttsCostController.dispose(); - aiSummaryCostController.dispose(); - dailyThresholdController.dispose(); - monthlyThresholdController.dispose(); - super.onClose(); - } - - /// 加载当前配置 - void _loadCurrentConfig() { - isLoading.value = true; - - try { - // 从服务获取当前配置 - costConfig.value = Map.from(_usageService.costConfig); - dailyCostThreshold.value = _usageService.dailyCostThreshold.value; - monthlyCostThreshold.value = _usageService.monthlyCostThreshold.value; - - // 更新表单控制器 - translationCostController.text = (costConfig['translation_per_1k_chars'] ?? 0.05).toString(); - asrCostController.text = (costConfig['asr_per_minute'] ?? 0.02).toString(); - ttsCostController.text = (costConfig['tts_per_1k_chars'] ?? 0.03).toString(); - aiSummaryCostController.text = (costConfig['ai_summary_per_request'] ?? 0.01).toString(); - dailyThresholdController.text = dailyCostThreshold.value.toString(); - monthlyThresholdController.text = monthlyCostThreshold.value.toString(); - - Logger.info('成本配置加载完成'); - } catch (e) { - Logger.error('加载成本配置失败: $e'); - _showErrorSnackbar('加载配置失败'); - } finally { - isLoading.value = false; - } - } - - /// 保存配置 - Future saveConfig() async { - if (isSaving.value) return; - - isSaving.value = true; - - try { - // 验证输入 - if (!_validateInputs()) { - return; - } - - // 更新成本配置 - final newCostConfig = { - 'translation_per_1k_chars': double.parse(translationCostController.text), - 'asr_per_minute': double.parse(asrCostController.text), - 'tts_per_1k_chars': double.parse(ttsCostController.text), - 'ai_summary_per_request': double.parse(aiSummaryCostController.text), - }; - - final newDailyThreshold = double.parse(dailyThresholdController.text); - final newMonthlyThreshold = double.parse(monthlyThresholdController.text); - - // 保存到服务 - _usageService.updateCostConfig(newCostConfig); - _usageService.updateThresholds( - dailyThreshold: newDailyThreshold, - monthlyThreshold: newMonthlyThreshold, - ); - - // 更新本地状态 - costConfig.value = newCostConfig; - dailyCostThreshold.value = newDailyThreshold; - monthlyCostThreshold.value = newMonthlyThreshold; - - _showSuccessSnackbar('配置保存成功'); - Logger.info('成本配置保存成功'); - - } catch (e) { - Logger.error('保存成本配置失败: $e'); - _showErrorSnackbar('保存配置失败'); - } finally { - isSaving.value = false; - } - } - - /// 重置为默认配置 - void resetToDefault() { - translationCostController.text = '0.05'; - asrCostController.text = '0.02'; - ttsCostController.text = '0.03'; - aiSummaryCostController.text = '0.01'; - dailyThresholdController.text = '10.0'; - monthlyThresholdController.text = '300.0'; - - _showInfoSnackbar('已重置为默认配置,请点击保存生效'); - } - - /// 验证输入 - bool _validateInputs() { - try { - final translationCost = double.parse(translationCostController.text); - final asrCost = double.parse(asrCostController.text); - final ttsCost = double.parse(ttsCostController.text); - final aiSummaryCost = double.parse(aiSummaryCostController.text); - final dailyThreshold = double.parse(dailyThresholdController.text); - final monthlyThreshold = double.parse(monthlyThresholdController.text); - - if (translationCost < 0 || asrCost < 0 || ttsCost < 0 || aiSummaryCost < 0) { - _showErrorSnackbar('成本不能为负数'); - return false; - } - - if (dailyThreshold < 0 || monthlyThreshold < 0) { - _showErrorSnackbar('预警阈值不能为负数'); - return false; - } - - if (dailyThreshold > monthlyThreshold) { - _showErrorSnackbar('日预警阈值不能大于月预警阈值'); - return false; - } - - return true; - } catch (e) { - _showErrorSnackbar('请输入有效的数字'); - return false; - } - } - - /// 计算预估月成本 - double calculateEstimatedMonthlyCost() { - try { - final dailyStats = _usageService.todayStats.value; - if (dailyStats.estimatedCost == 0) return 0.0; - - // 简单按30天计算 - return dailyStats.estimatedCost * 30; - } catch (e) { - Logger.error('计算预估月成本失败: $e'); - return 0.0; - } - } - - /// 获取成本节约建议 - List getCostSavingTips() { - final tips = []; - - try { - final dailyStats = _usageService.todayStats.value; - - // 基于使用情况给出建议 - if (dailyStats.translationApiCalls > 100) { - tips.add('考虑批量翻译以减少API调用次数'); - } - - if (dailyStats.ttsApiCalls > 50) { - tips.add('可以关闭自动语音播放以节省TTS成本'); - } - - if (dailyStats.aiSummaryApiCalls > 10) { - tips.add('AI摘要功能按需使用,避免频繁调用'); - } - - if (dailyStats.effectiveAudioDuration > 3600) { // 超过1小时 - tips.add('长时间录音会增加ASR成本,建议分段录音'); - } - - if (tips.isEmpty) { - tips.add('您的使用习惯很好,继续保持!'); - } - - } catch (e) { - Logger.error('获取成本节约建议失败: $e'); - tips.add('暂无建议'); - } - - return tips; - } - - /// 显示成功提示 - void _showSuccessSnackbar(String message) { - Get.snackbar( - '成功', - message, - snackPosition: SnackPosition.TOP, - backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1), - colorText: Get.theme.colorScheme.primary, - duration: const Duration(seconds: 3), - ); - } - - /// 显示错误提示 - void _showErrorSnackbar(String message) { - Get.snackbar( - '错误', - message, - snackPosition: SnackPosition.TOP, - backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1), - colorText: Get.theme.colorScheme.error, - duration: const Duration(seconds: 3), - ); - } - - /// 显示信息提示 - void _showInfoSnackbar(String message) { - Get.snackbar( - '提示', - message, - snackPosition: SnackPosition.TOP, - backgroundColor: Get.theme.colorScheme.surface.withOpacity(0.9), - colorText: Get.theme.colorScheme.onSurface, - duration: const Duration(seconds: 3), - ); - } - - /// 格式化成本显示 - String formatCost(double cost) { - return '¥${cost.toStringAsFixed(2)}'; - } - - /// 格式化百分比显示 - String formatPercentage(double percentage) { - return '${percentage.toStringAsFixed(1)}%'; - } -} diff --git a/lib/modules/translation/controllers/translation_detail_controller.dart b/lib/modules/translation/controllers/translation_detail_controller.dart index da34f3ff2..e2d965b0e 100644 --- a/lib/modules/translation/controllers/translation_detail_controller.dart +++ b/lib/modules/translation/controllers/translation_detail_controller.dart @@ -7,14 +7,14 @@ import '../models/translation_models.dart'; // 添加这行导入 import 'package:get_storage/get_storage.dart'; import 'dart:async'; import '../../../core/utils/logger.dart'; -import '../../../data/services/translation_usage_service.dart'; +import '../../../data/services/usage_stats_service.dart'; //详情页管理 class TranslationDetailController extends GetxController { // 依赖注入 //final VolcanoAIService _aiService = Get.find(); final GetStorage _storage = GetStorage(); - late final TranslationUsageService _usageService; + late final UsageStatsService _usageService; // 存储键前缀 static const String _summaryKeyPrefix = 'translation_summary_'; @@ -40,12 +40,12 @@ class TranslationDetailController extends GetxController { // 初始化统计服务 try { - if (!Get.isRegistered()) { - Get.put(TranslationUsageService(), permanent: true); + if (!Get.isRegistered()) { + Get.put(UsageStatsService(), permanent: true); } - _usageService = Get.find(); + _usageService = Get.find(); } catch (e) { - Logger.error('初始化统计服务失败: $e'); + Logger.error('初始化使用量统计服务失败: $e'); } // 从路由参数中获取数据 diff --git a/lib/modules/translation/controllers/translation_history_controller.dart b/lib/modules/translation/controllers/translation_history_controller.dart index b7e7d15b3..f94f1f7a6 100644 --- a/lib/modules/translation/controllers/translation_history_controller.dart +++ b/lib/modules/translation/controllers/translation_history_controller.dart @@ -15,6 +15,12 @@ class TranslationHistoryManager extends GetxController { static const int _maxStorageItems = 1000; // 存储限制 static const int _maxDisplayItems = 20; // 显示限制 + String _currentMode = 'simultaneous'; // 当前模式 + String _getHistoryKey() => '${_historyKey}_$_currentMode'; // 模式化存储键 + void setMode(String mode) { + _currentMode = mode; + } + final GetStorage _storage = GetStorage(); // 翻译历史数据 @@ -40,7 +46,7 @@ class TranslationHistoryManager extends GetxController { /// 加载翻译历史记录 /// 初始化时只加载最近的显示条数,提高性能 void loadTranslationHistory() { - final historyJson = _storage.read(_historyKey); + final historyJson = _storage.read(_getHistoryKey()); if (historyJson != null) { try { final List historyList = jsonDecode(historyJson); @@ -64,15 +70,15 @@ class TranslationHistoryManager extends GetxController { Logger.info('翻译历史加载成功: ${translationHistory.length}条记录'); } catch (e) { - _storage.remove(_historyKey); + _storage.remove(_getHistoryKey()); Logger.error('加载翻译历史失败: ${e.toString()}'); } } else { - // 没有历史数据时初始化分页状态 hasMoreHistory.value = false; currentPage.value = 1; isLoadingMore.value = false; isLoadingMoreError.value = false; + translationHistory.clear(); } } @@ -94,7 +100,7 @@ class TranslationHistoryManager extends GetxController { // 保存到本地存储 final List> historyJson = historyToSave.map((item) => item.toJson()).toList(); - _storage.write(_historyKey, jsonEncode(historyJson)); + _storage.write(_getHistoryKey(), jsonEncode(historyJson)); Logger.info('翻译历史保存成功: 存储${historyToSave.length}条记录'); } catch (e) { @@ -141,7 +147,7 @@ class TranslationHistoryManager extends GetxController { Logger.i(TAG, '正在加载第 $nextPage 页翻译历史...'); // 从存储中获取所有历史记录 - final historyJson = _storage.read(_historyKey); + final historyJson = _storage.read(_getHistoryKey()); if (historyJson != null) { final List historyList = jsonDecode(historyJson); final List allHistory = @@ -194,8 +200,8 @@ class TranslationHistoryManager extends GetxController { final List result = []; final Set existingIds = {}; - // 添加存储的历史记录 - final historyJson = _storage.read(_historyKey); + // 添加存储的历史记录(按当前模式) + final historyJson = _storage.read(_getHistoryKey()); if (historyJson != null) { final List historyList = jsonDecode(historyJson); final List storedHistory = @@ -235,7 +241,7 @@ class TranslationHistoryManager extends GetxController { /// 清除所有翻译历史 void clearTranslationHistory() { translationHistory.clear(); - _storage.remove(_historyKey); + _storage.remove(_getHistoryKey()); Logger.info('翻译历史已清除'); } @@ -247,8 +253,8 @@ class TranslationHistoryManager extends GetxController { translationHistory.removeWhere((item) => item.sessionId == sessionId); final removedFromMemory = originalCount - translationHistory.length; - // 从存储中删除 - final historyJson = _storage.read(_historyKey); + // 从存储中删除(按当前模式) + final historyJson = _storage.read(_getHistoryKey()); int removedFromStorage = 0; if (historyJson != null) { @@ -263,7 +269,7 @@ class TranslationHistoryManager extends GetxController { // 重新保存 final List> updatedHistoryJson = storedHistory.map((item) => item.toJson()).toList(); - _storage.write(_historyKey, jsonEncode(updatedHistoryJson)); + _storage.write(_getHistoryKey(), jsonEncode(updatedHistoryJson)); } translationHistory.refresh(); @@ -287,8 +293,8 @@ class TranslationHistoryManager extends GetxController { timestamp.millisecondsSinceEpoch); final removedFromMemory = originalCount - translationHistory.length; - // 从存储中删除 - final historyJson = _storage.read(_historyKey); + // 从存储中删除(按当前模式) + final historyJson = _storage.read(_getHistoryKey()); int removedFromStorage = 0; if (historyJson != null) { @@ -306,7 +312,7 @@ class TranslationHistoryManager extends GetxController { // 重新保存 final List> updatedHistoryJson = storedHistory.map((item) => item.toJson()).toList(); - _storage.write(_historyKey, jsonEncode(updatedHistoryJson)); + _storage.write(_getHistoryKey(), jsonEncode(updatedHistoryJson)); } translationHistory.refresh(); diff --git a/lib/modules/translation/middleware/controllers/translation_middleware_controller.dart b/lib/modules/translation/middleware/controllers/translation_middleware_controller.dart index 614d8f811..ad9473fb9 100644 --- a/lib/modules/translation/middleware/controllers/translation_middleware_controller.dart +++ b/lib/modules/translation/middleware/controllers/translation_middleware_controller.dart @@ -75,13 +75,23 @@ class TranslationMiddlewareController extends GetxController { iconColor: Colors.indigo[500]!, // 主靛 targetRoute: Routes.textTranslation, ), + TranslationOption( + translationId: 'voice_replication', + title: '声音复刻', + icon: Icons.volume_up, + startColor: Colors.pink[400]!, // 粉色 + endColor: Colors.pink[600]!, // 深粉 + iconBgColor: Colors.white, // 白色 + iconColor: Colors.pink[500]!, // 主粉 + targetRoute: Routes.voiceReplication, + ), ]; /// 导航到指定的翻译功能 void navigateToTranslationFeature(TranslationOption option) { // 不可用则提示用户连接设备 if (!isOptionEnabled(option)) { - // 翻译键代表的中文文本内容 + // 连接设备 / 请连接 Voitrans 设备 Get.snackbar('connectDevice'.tr, 'pleaseConnectDeepSoundDevice'.tr, // 连接设备 / 请连接 Voitrans 设备 snackPosition: SnackPosition.TOP, diff --git a/lib/modules/translation/middleware/views/translation_middleware_view.dart b/lib/modules/translation/middleware/views/translation_middleware_view.dart index adc9d95cb..9e1de5067 100644 --- a/lib/modules/translation/middleware/views/translation_middleware_view.dart +++ b/lib/modules/translation/middleware/views/translation_middleware_view.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/translation_middleware_controller.dart'; import '../../../../core/theme/app_colors.dart'; +import '../../../../data/models/user_Info.dart'; /// 翻译中间层视图 /// 展示四种翻译功能的入口选项 @@ -44,6 +45,63 @@ class TranslationMiddlewareView color: isDarkMode ? Colors.white : Colors.black, ), ), + bottom: PreferredSize( + preferredSize: Size.fromHeight(28.w), + child: Obx(() { + final _ = User.refreshTick.value; + final remain = User.isLoggedIn() ? User.instance.tradeintegral : 0; + final total = + User.isLoggedIn() ? User.instance.tradetotalintegral : 0; + final double fraction = + total > 0 ? (remain / total).clamp(0.0, 1.0).toDouble() : 0.0; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: EdgeInsets.only(right: 12.w, bottom: 4.w), + alignment: Alignment.centerRight, + child: Text( + _formatHms(remain), + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ), + LayoutBuilder( + builder: (context, constraints) { + final barWidth = constraints.maxWidth * fraction; + return Stack( + children: [ + Container( + height: 2.w, + decoration: BoxDecoration( + color: isDarkMode ? Colors.white10 : Colors.black12, + borderRadius: BorderRadius.circular(2.w), + ), + ), + Align( + alignment: Alignment.centerRight, + child: Container( + width: barWidth, + height: 2.w, + decoration: BoxDecoration( + color: isDarkMode + ? const Color(0xFF0066CC) + : Colors.blue, + borderRadius: BorderRadius.circular(2.w), + ), + ), + ), + ], + ); + }, + ), + ], + ); + }), + ), ); } @@ -152,4 +210,14 @@ class TranslationMiddlewareView ), ); } + + String _formatHms(int seconds) { + final sec = seconds < 0 ? 0 : seconds; // 确保秒数非负 + final d = Duration(seconds: sec); + final h = d.inHours; + final m = d.inMinutes.remainder(60); + final s = d.inSeconds.remainder(60); + String twoDigits(int n) => n.toString().padLeft(2, '0'); + return '${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}'; + } } diff --git a/lib/modules/translation/views/dialogs/language_selection_dialog.dart b/lib/modules/translation/views/dialogs/language_selection_dialog.dart index 6bd5be25d..ccef8f120 100644 --- a/lib/modules/translation/views/dialogs/language_selection_dialog.dart +++ b/lib/modules/translation/views/dialogs/language_selection_dialog.dart @@ -65,8 +65,8 @@ class LanguageSelectionDialog { padding: EdgeInsets.symmetric(horizontal: 8.w), child: Text( isSource - ? 'selectSourceLanguage'.tr // 翻译键代表的中文文本内容 - : 'selectTargetLanguage'.tr, // 翻译键代表的中文文本内容 + ? 'selectSourceLanguage'.tr // 选择源语言 + : 'selectTargetLanguage'.tr, // 选择目标语言 style: TextStyle( fontSize: 18.sp, // 调整字号 fontWeight: FontWeight.w600, // 使用数字字重 @@ -94,7 +94,7 @@ class LanguageSelectionDialog { : displayLanguages[ hasAutoDetect ? index - 1 : index]; final displayText = isAutoDetectItem - ? 'autoDetect'.tr // 翻译键代表的中文文本内容 + ? 'autoDetect'.tr // 自动检测 : (translateLanguageNames ? valueText.tr : valueText); @@ -196,7 +196,7 @@ class LanguageSelectionDialog { child: TextButton( onPressed: () => Get.back(), child: Text( - 'cancel'.tr, // 翻译键代表的中文文本内容 + 'cancel'.tr, // 取消 style: TextStyle( color: isDarkMode ? const Color(0xFF6200EE) : Colors.blue, ), diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index b51cf7fa0..75df6b81b 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -80,6 +80,31 @@ class TranslationView extends GetView { controller: controller, ), + Positioned( + top: 2, + left: 0, + right: 0, + child: SafeArea( + top: true, + child: Container( + height: 56.h, + padding: EdgeInsets.symmetric(horizontal: 8.w), + color: Colors.transparent, + child: Align( + alignment: Alignment.centerLeft, + child: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), + ), + ), + ), + ), + ), + // 顶部按钮(倒过来) Positioned( top: 50.h, @@ -210,28 +235,27 @@ class TranslationView extends GetView { return Row( children: [ // 悬浮窗切换按钮(仅在支持的模式下显示) - if (Platform.isAndroid) - Obx(() { - if (controller.currentMode.value == 'call' || - controller.currentMode.value == 'audioVideo') { - return IconButton( - icon: Icon( - controller.isFloatingWindowEnabled.value - ? Icons.picture_in_picture - : Icons.picture_in_picture_alt_outlined, - color: controller.isFloatingWindowEnabled.value - ? Colors.blue - : (isDarkMode ? Colors.white : Colors.black), - size: 24.sp, - ), - onPressed: controller.toggleFloatingWindow, - tooltip: controller.isFloatingWindowEnabled.value - ? '禁用悬浮窗' - : '启用悬浮窗', - ); - } - return const SizedBox.shrink(); - }), + Obx(() { + if (controller.currentMode.value == 'call' || + controller.currentMode.value == 'audioVideo') { + return IconButton( + icon: Icon( + controller.isFloatingWindowEnabled.value + ? Icons.picture_in_picture + : Icons.picture_in_picture_alt_outlined, + color: controller.isFloatingWindowEnabled.value + ? Colors.blue + : (isDarkMode ? Colors.white : Colors.black), + size: 24.sp, + ), + onPressed: controller.toggleFloatingWindow, + tooltip: controller.isFloatingWindowEnabled.value + ? '禁用悬浮窗' + : '启用悬浮窗', + ); + } + return const SizedBox.shrink(); + }), IconButton( icon: Icon( Icons.radio_button_checked, @@ -290,7 +314,7 @@ class TranslationView extends GetView { onTapTarget: () => LanguageSelectionDialog.show(false, isDarkMode), onSwap: controller.swapLanguages, - translateLanguageNames: true, // 翻译键代表的中文文本内容 + translateLanguageNames: true, // 是否翻译语言名称 fontSizeSp: 15, targetSuffix: controller.currentMode.value == 'call' ? Icon( @@ -443,134 +467,122 @@ class TranslationView extends GetView { ), ], ), - child: Obx(() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 原有的按钮区域 - if (controller.currentMode.value == 'faceToFace') - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 耳机图标按钮 (说话者1)-显示源语言 - Obx(() => _buildHoldToTalkButton( - isDarkMode: isDarkMode, - speakerId: 1, - activeSpeaker: controller.activeSpeaker.value, - icon: Icons.mic_none, - label: controller.sourceLanguage.value - .split(' ')[0] - .tr, // 源语言名称 - )), - SizedBox(width: 16.w), - // 手机图标按钮 (说话者2)-显示目标语言 - Obx(() => _buildHoldToTalkButton( - isDarkMode: isDarkMode, - speakerId: 2, - activeSpeaker: controller.activeSpeaker.value, - icon: Icons.mic_none, - label: controller.targetLanguage.value - .split(' ')[0] - .tr, // 目标语言名称 - )), - ], - ) - else - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 主要控制按钮 - 开始/暂停语音识别 - Obx(() { - final isRecognizing = controller.isRecognizing.value; - return GestureDetector( - onTap: () { - if (isRecognizing) { - controller.stopAll(); - } else { - controller.startRecognition(); - } - }, - child: Container( - width: 60.w, - height: 60.w, - decoration: BoxDecoration( - color: isRecognizing - ? (isDarkMode ? Colors.red[700] : Colors.red) - : (isDarkMode ? AppColors.primary : Colors.blue), - shape: BoxShape.circle, - ), - child: Icon( - isRecognizing ? Icons.pause : Icons.play_arrow, - color: Colors.white, - size: 30.sp, - ), - ), - ); - }), - ], - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 原有的按钮区域 + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 主要控制按钮 - 开始/暂停语音识别 + Obx(() { + final isRecognizing = controller.isRecognizing.value; + final isPreparing = controller.isPreparing.value; + return GestureDetector( + onTap: () { + if (isRecognizing) { + controller.stopAll(); + } else { + controller.startRecognition(); + } + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + width: 60.w, + height: 60.w, + transform: isPreparing + ? (Matrix4.identity()..scale(1.08)) + : Matrix4.identity(), + decoration: BoxDecoration( + color: isRecognizing + ? (isDarkMode ? Colors.red[700] : Colors.red) + : (isDarkMode ? AppColors.primary : Colors.blue), + shape: BoxShape.circle, + boxShadow: isPreparing + ? [ + BoxShadow( + color: (isDarkMode + ? AppColors.primary + : Colors.blue) + .withOpacity(0.5), + blurRadius: 12, + spreadRadius: 2, + ) + ] + : [], + ), + child: Icon( + isRecognizing ? Icons.pause : Icons.play_arrow, + color: Colors.white, + size: 30.sp, + ), + ), + ); + }), + ], + ), - // 添加间距 - //SizedBox(height: 16.h), - - // 语音识别结果显示框 - // Container( - // width: double.infinity, - // padding: EdgeInsets.all(12.w), - // decoration: BoxDecoration( - // color: isDarkMode ? Colors.grey[800] : Colors.grey[100], - // borderRadius: BorderRadius.circular(8.r), - // border: Border.all( - // color: isDarkMode ? Colors.grey[600]! : Colors.grey[300]!, - // width: 1, - // ), - // ), - // child: Obx(() { - // // 显示最新的语音识别结果 - // final recognitionText = controller.latestRecognitionText.value; - // final detectedLanguage = controller.detectedLanguageCode1.value; - - // return Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // // 标题 - // Text( - // '麦头语音识别结果', - // style: TextStyle( - // fontSize: 12.sp, - // color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - // fontWeight: FontWeight.w500, - // ), - // ), - // SizedBox(height: 4.h), - // // 识别文本 - 这里显示 finalResult1 的内容 - // Text( - // recognitionText.isEmpty ? '等待语音输入...' : recognitionText, - // style: TextStyle( - // fontSize: 14.sp, - // color: isDarkMode ? Colors.white : Colors.black87, - // height: 1.3, - // ), - // ), - // // 检测到的语言(如果有) - // if (detectedLanguage.isNotEmpty) ...[ - // SizedBox(height: 4.h), - // Text( - // '检测语言: $detectedLanguage', - // style: TextStyle( - // fontSize: 10.sp, - // color: - // isDarkMode ? Colors.grey[500] : Colors.grey[500], - // ), - // ), - // ], - // ], - // ); - // }), - // ), - ], - ); - }), + // 添加间距 + //SizedBox(height: 16.h), + + // 语音识别结果显示框 + // Container( + // width: double.infinity, + // padding: EdgeInsets.all(12.w), + // decoration: BoxDecoration( + // color: isDarkMode ? Colors.grey[800] : Colors.grey[100], + // borderRadius: BorderRadius.circular(8.r), + // border: Border.all( + // color: isDarkMode ? Colors.grey[600]! : Colors.grey[300]!, + // width: 1, + // ), + // ), + // child: Obx(() { + // // 显示最新的语音识别结果 + // final recognitionText = controller.latestRecognitionText.value; + // final detectedLanguage = controller.detectedLanguageCode1.value; + + // return Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // // 标题 + // Text( + // '麦头语音识别结果', + // style: TextStyle( + // fontSize: 12.sp, + // color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + // fontWeight: FontWeight.w500, + // ), + // ), + // SizedBox(height: 4.h), + // // 识别文本 - 这里显示 finalResult1 的内容 + // Text( + // recognitionText.isEmpty ? '等待语音输入...' : recognitionText, + // style: TextStyle( + // fontSize: 14.sp, + // color: isDarkMode ? Colors.white : Colors.black87, + // height: 1.3, + // ), + // ), + // // 检测到的语言(如果有) + // if (detectedLanguage.isNotEmpty) ...[ + // SizedBox(height: 4.h), + // Text( + // '检测语言: $detectedLanguage', + // style: TextStyle( + // fontSize: 10.sp, + // color: + // isDarkMode ? Colors.grey[500] : Colors.grey[500], + // ), + // ), + // ], + // ], + // ); + // }), + // ), + ], + ), ); } @@ -1007,7 +1019,7 @@ class TranslationView extends GetView { onTapSource: () => LanguageSelectionDialog.show(true, isDarkMode), onTapTarget: () => LanguageSelectionDialog.show(false, isDarkMode), onSwap: controller.swapLanguages, - translateLanguageNames: true, // 翻译键代表的中文文本内容 + translateLanguageNames: true, // 是否翻译语言名称 fontSizeSp: 15, ), ); diff --git a/lib/modules/translation/views/widgets/face_to_face_layout.dart b/lib/modules/translation/views/widgets/face_to_face_layout.dart index 562a543b9..efc6140a3 100644 --- a/lib/modules/translation/views/widgets/face_to_face_layout.dart +++ b/lib/modules/translation/views/widgets/face_to_face_layout.dart @@ -7,7 +7,7 @@ import '../../../../core/theme/app_colors.dart'; import '../dialogs/language_selection_dialog.dart'; /// 面对面翻译布局组件 -class FaceToFaceLayout extends StatelessWidget { +class FaceToFaceLayout extends StatefulWidget { final bool isDarkMode; final TranslationController controller; @@ -17,13 +17,60 @@ class FaceToFaceLayout extends StatelessWidget { required this.controller, }) : super(key: key); + @override + State createState() => _FaceToFaceLayoutState(); +} + +class _FaceToFaceLayoutState extends State { + final ScrollController _topScrollController = ScrollController(); + final ScrollController _bottomScrollController = ScrollController(); + + @override + void dispose() { + _topScrollController.dispose(); + _bottomScrollController.dispose(); + super.dispose(); + } + + void _scrollToBottom({bool animate = false}) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_topScrollController.hasClients) { + final m = _topScrollController.position.maxScrollExtent; + if (animate) { + _topScrollController.animateTo( + m, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + } else { + _topScrollController.jumpTo(m); + } + } + if (_bottomScrollController.hasClients) { + final m2 = _bottomScrollController.position.maxScrollExtent; + if (animate) { + _bottomScrollController.animateTo( + m2, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + } else { + _bottomScrollController.jumpTo(m2); + } + } + }); + } + @override Widget build(BuildContext context) { + final isDarkMode = widget.isDarkMode; + final controller = widget.controller; return Obx(() { // 获取最新的翻译记录 final latestItem = controller.translationHistory.isNotEmpty ? controller.translationHistory.last : null; + _scrollToBottom(animate: false); return Column( children: [ @@ -33,7 +80,7 @@ class FaceToFaceLayout extends StatelessWidget { angle: 3.14159, // 180度 = π弧度 child: Container( width: double.infinity, - padding: EdgeInsets.all(24.w), + padding: EdgeInsets.fromLTRB(24.w, 24.w, 24.w, 24.w + 96.h), decoration: BoxDecoration( color: isDarkMode ? AppColors.cardBackground.withValues(alpha: 0.5) @@ -45,24 +92,22 @@ class FaceToFaceLayout extends StatelessWidget { ), ), ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 上半部的文本 - Text( - latestItem?.targetLanguageCode == - controller.targetLanguageCode.value - ? latestItem?.translatedText ?? '' //如果为null则显示空字符 - : latestItem?.sourceText ?? '', - style: TextStyle( - fontSize: 20.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : Colors.black87, - height: 1.4, - ), - textAlign: TextAlign.center, + child: SingleChildScrollView( + controller: _topScrollController, + physics: const BouncingScrollPhysics(), + child: Text( + latestItem?.targetLanguageCode == + controller.targetLanguageCode.value + ? latestItem?.translatedText ?? '' + : latestItem?.sourceText ?? '', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + height: 1.4, ), - ], + textAlign: TextAlign.center, + ), ), ), ), @@ -75,33 +120,31 @@ class FaceToFaceLayout extends StatelessWidget { Expanded( child: Container( width: double.infinity, - padding: EdgeInsets.all(24.w), + padding: EdgeInsets.fromLTRB(24.w, 24.w, 24.w, 24.w + 96.h), decoration: BoxDecoration( color: isDarkMode ? AppColors.cardBackground.withValues(alpha: 0.5) : Colors.green[50]?.withValues(alpha: 0.3), ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 下半部的文本 - Text( - latestItem?.sourceLanguageCode == - controller.sourceLanguageCode.value - ? latestItem?.sourceText ?? - (controller.faceToFaceRecognitionText.value.isEmpty - ? '' - : controller.faceToFaceRecognitionText.value) - : latestItem?.translatedText ?? '', - style: TextStyle( - fontSize: 20.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : Colors.black87, - height: 1.4, - ), - textAlign: TextAlign.center, + child: SingleChildScrollView( + controller: _bottomScrollController, + physics: const BouncingScrollPhysics(), + child: Text( + latestItem?.sourceLanguageCode == + controller.sourceLanguageCode.value + ? latestItem?.sourceText ?? + (controller.faceToFaceRecognitionText.value.isEmpty + ? '' + : controller.faceToFaceRecognitionText.value) + : latestItem?.translatedText ?? '', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + height: 1.4, ), - ], + textAlign: TextAlign.center, + ), ), ), ), @@ -146,7 +189,7 @@ class FaceToFaceLayout extends StatelessWidget { borderRadius: BorderRadius.circular(16.r), ), child: Text( - '${controller.sourceLanguage.value.split(' ')[0]}'.tr, + '${widget.controller.sourceLanguage.value.split(' ')[0]}'.tr, style: TextStyle( fontSize: 15.sp, fontWeight: FontWeight.w600, @@ -161,7 +204,7 @@ class FaceToFaceLayout extends StatelessWidget { // 语言互换按钮 GestureDetector( - onTap: controller.swapLanguages, + onTap: widget.controller.swapLanguages, child: Container( margin: EdgeInsets.symmetric(horizontal: 8.w), padding: EdgeInsets.all(4.r), @@ -194,7 +237,8 @@ class FaceToFaceLayout extends StatelessWidget { child: Transform.rotate( angle: 3.14159, // 旋转180度 child: Text( - '${controller.targetLanguage.value.split(' ')[0]}'.tr, + '${widget.controller.targetLanguage.value.split(' ')[0]}' + .tr, style: TextStyle( fontSize: 15.sp, fontWeight: FontWeight.w600, diff --git a/lib/modules/translation/views/widgets/translation_item_widget.dart b/lib/modules/translation/views/widgets/translation_item_widget.dart index 49ce94472..38840b00c 100644 --- a/lib/modules/translation/views/widgets/translation_item_widget.dart +++ b/lib/modules/translation/views/widgets/translation_item_widget.dart @@ -49,15 +49,19 @@ class TranslationItemWidget extends GetView { // 判断是否是己方的翻译项(依据控制器当前源语言) final bool isSelf = item.sourceLanguageCode == controller.sourceLanguageCode.value; + // 新增:通话模式标记,用于控制右侧显示与隐藏中间态 + final bool isCallMode = controller.currentMode.value == 'call'; return GestureDetector( onTap: () => onToggleActionItem(item), // 触碰气泡时切换按钮显隐 onLongPressStart: (details) => onLongPressStart(context, isDarkMode, details, item), child: Align( - alignment: item.isIntermediate - ? Alignment.center - : (isSelf ? Alignment.centerLeft : Alignment.centerRight), + alignment: isCallMode + ? (isSelf ? Alignment.centerLeft : Alignment.centerRight) + : item.isIntermediate + ? Alignment.center + : (isSelf ? Alignment.centerLeft : Alignment.centerRight), child: Container( margin: EdgeInsets.only(bottom: 8.h), padding: EdgeInsets.all(12.w), @@ -136,7 +140,7 @@ class TranslationItemWidget extends GetView { child: Container( padding: EdgeInsets.all(4.w), child: Tooltip( - message: 'copy'.tr, // 翻译键代表的中文文本内容 + message: 'copy'.tr, // 复制 child: Icon( Icons.copy, size: 20.sp, @@ -165,7 +169,7 @@ class TranslationItemWidget extends GetView { child: Container( padding: EdgeInsets.all(4.w), child: Tooltip( - message: 'delete'.tr, // 翻译键代表的中文文本内容 + message: 'delete'.tr, // 删除 child: Icon( Icons.delete_outline, size: 20.sp, diff --git a/lib/modules/usage/bindings/usage_binding.dart b/lib/modules/usage/bindings/usage_binding.dart new file mode 100644 index 000000000..e03af7554 --- /dev/null +++ b/lib/modules/usage/bindings/usage_binding.dart @@ -0,0 +1,23 @@ +import 'package:get/get.dart'; +import '../controllers/usage_controller.dart'; +import '../controllers/meeting_stats_controller.dart'; +import '../controllers/translation_stats_controller.dart'; +import '../controllers/aichat_stats_controller.dart'; +import '../../../data/services/usage_stats_service.dart'; + +class UsageBinding implements Bindings { + @override + void dependencies() { + if (!Get.isRegistered()) { + Get.put(UsageStatsService(), permanent: true); + } + + Get.lazyPut(() => UsageController()); + + Get.lazyPut(() => AiChatStatsController()); + + Get.lazyPut(() => TranslationStatsController()); + + Get.lazyPut(() => MeetingStatsController()); + } +} diff --git a/lib/modules/usage/controllers/aichat_stats_controller.dart b/lib/modules/usage/controllers/aichat_stats_controller.dart new file mode 100644 index 000000000..efc285a6c --- /dev/null +++ b/lib/modules/usage/controllers/aichat_stats_controller.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; +import '../../../data/models/user_Info.dart'; + +class AiChatStatsController extends GetxController { + final isLoading = false.obs; + + int get currentIntegral => User.instance.aichatintegral; + int get totalIntegral => User.instance.aichattotalintegral; +} + diff --git a/lib/modules/usage/controllers/meeting_stats_controller.dart b/lib/modules/usage/controllers/meeting_stats_controller.dart new file mode 100644 index 000000000..2aad0d20a --- /dev/null +++ b/lib/modules/usage/controllers/meeting_stats_controller.dart @@ -0,0 +1,226 @@ +import 'package:get/get.dart'; +import '../../../data/services/usage_stats_service.dart'; +import '../../../data/models/meeting_usage_stats.dart'; +import '../../../core/utils/logger.dart'; + +class MeetingStatsController extends GetxController { + final UsageStatsService _usageService = Get.find(); + + final isLoading = false.obs; + final selectedTabIndex = 0.obs; + final selectedTimeRange = 'today'.tr.obs; // 今日 + + final Rx todayStats = MeetingAsrHistoryStats( + userId: '', + allAsrCalls: 0, + allAudioSeconds: 0, + allAudioFileBytes: 0, + allTranscriptBytes: 0, + ).obs; + final Rx historyStats = MeetingAsrHistoryStats( + userId: '', + allAsrCalls: 0, + allAudioSeconds: 0, + allAudioFileBytes: 0, + allTranscriptBytes: 0, + ).obs; + + final RxList historyDaily = + [].obs; + + final Rx todayStorageStats = + MeetingStorageHistoryStats( + userId: '', + allStorageUploads: 0, + allStorageBytes: 0, + allTransferBytes: 0, + ).obs; + + final Rx historyStorageStats = + MeetingStorageHistoryStats( + userId: '', + allStorageUploads: 0, + allStorageBytes: 0, + allTransferBytes: 0, + ).obs; + + final Rx rangeStart = Rx(null); + final Rx rangeEnd = Rx(null); + final Rx rangeStats = MeetingAsrHistoryStats( + userId: '', + allAsrCalls: 0, + allAudioSeconds: 0, + allAudioFileBytes: 0, + allTranscriptBytes: 0, + ).obs; + final Rx rangeStorageStats = + MeetingStorageHistoryStats( + userId: '', + allStorageUploads: 0, + allStorageBytes: 0, + allTransferBytes: 0, + ).obs; + + @override + void onInit() { + super.onInit(); + loadAllStats(); + } + + Future loadAllStats() async { + isLoading.value = true; + try { + final now = DateTime.now(); + + final todayList = await _usageService.getMeetingHistoryStats( + startDate: DateTime(now.year, now.month, now.day), + endDate: DateTime(now.year, now.month, now.day), + ); + todayStats.value = _usageService.aggregateMeetingStats(todayList); + + final historyStart = now.subtract(const Duration(days: 7)); + historyDaily.value = await _usageService.getMeetingHistoryStats( + startDate: + DateTime(historyStart.year, historyStart.month, historyStart.day), + endDate: DateTime(now.year, now.month, now.day), + ); + historyStats.value = + _usageService.aggregateMeetingStats(historyDaily.value); + + final todayStorageDaily = + await _usageService.getMeetingStorageHistoryStats( + startDate: DateTime(now.year, now.month, now.day), + endDate: DateTime(now.year, now.month, now.day), + ); + todayStorageStats.value = + _usageService.aggregateMeetingStorageStats(todayStorageDaily); + + final historyStorageDaily = + await _usageService.getMeetingStorageHistoryStats( + startDate: + DateTime(historyStart.year, historyStart.month, historyStart.day), + endDate: DateTime(now.year, now.month, now.day), + ); + historyStorageStats.value = + _usageService.aggregateMeetingStorageStats(historyStorageDaily); + + if (rangeStart.value != null && rangeEnd.value != null) { + final customAsrDaily = await _usageService.getMeetingHistoryStats( + startDate: DateTime(rangeStart.value!.year, rangeStart.value!.month, + rangeStart.value!.day), + endDate: DateTime( + rangeEnd.value!.year, rangeEnd.value!.month, rangeEnd.value!.day), + ); + rangeStats.value = _usageService.aggregateMeetingStats(customAsrDaily); + final customStorageDaily = + await _usageService.getMeetingStorageHistoryStats( + startDate: DateTime(rangeStart.value!.year, rangeStart.value!.month, + rangeStart.value!.day), + endDate: DateTime( + rangeEnd.value!.year, rangeEnd.value!.month, rangeEnd.value!.day), + ); + rangeStorageStats.value = + _usageService.aggregateMeetingStorageStats(customStorageDaily); + } else { + rangeStats.value = historyStats.value; + rangeStorageStats.value = historyStorageStats.value; + } + + Logger.info('会议统计数据加载完成'); + } catch (e) { + Logger.error('加载会议统计数据失败: $e'); + } finally { + isLoading.value = false; + } + } + + void switchTimeRange(int index) { + selectedTabIndex.value = index; + switch (index) { + case 0: + selectedTimeRange.value = 'today'.tr; // 今日 + break; + case 1: + selectedTimeRange.value = 'history'.tr; // 历史 + break; + default: + selectedTimeRange.value = 'today'.tr; // 今日 + break; + } + } + + MeetingAsrHistoryStats get currentStats { + switch (selectedTabIndex.value) { + case 0: + return todayStats.value; + case 1: + return rangeStats.value; + default: + return todayStats.value; + } + } + + String formatDuration(int seconds) { + if (seconds < 60) { + return '$seconds${'seconds'.tr}'; // 秒 + } else if (seconds < 3600) { + final minutes = seconds ~/ 60; + final remainingSeconds = seconds % 60; + return '$minutes${'minutes'.tr}$remainingSeconds${'seconds'.tr}'; // 分钟秒 + } else { + final hours = seconds ~/ 3600; + final minutes = (seconds % 3600) ~/ 60; + return '$hours${'hours'.tr}$minutes${'minutes'.tr}'; // 小时分钟 + } + } + + String formatBytes(int bytes) { + if (bytes < 1024) return '$bytes B'; + final kb = bytes / 1024; + if (kb < 1024) return '${kb.toStringAsFixed(1)} KB'; + final mb = kb / 1024; + if (mb < 1024) return '${mb.toStringAsFixed(1)} MB'; + final gb = mb / 1024; + return '${gb.toStringAsFixed(1)} GB'; + } + + String formatNumber(int number) { + if (number < 1000) return number.toString(); + if (number < 1000000) return '${(number / 1000).toStringAsFixed(1)}K'; + return '${(number / 1000000).toStringAsFixed(1)}M'; + } + + Future refreshData() async { + await loadAllStats(); + } + + Future setCustomRangeDates(DateTime start, DateTime end) async { + rangeStart.value = start; + rangeEnd.value = end; + isLoading.value = true; + try { + final asrDaily = await _usageService.getMeetingHistoryStats( + startDate: DateTime(start.year, start.month, start.day), + endDate: DateTime(end.year, end.month, end.day), + ); + rangeStats.value = _usageService.aggregateMeetingStats(asrDaily); + final storageDaily = await _usageService.getMeetingStorageHistoryStats( + startDate: DateTime(start.year, start.month, start.day), + endDate: DateTime(end.year, end.month, end.day), + ); + rangeStorageStats.value = + _usageService.aggregateMeetingStorageStats(storageDaily); + } catch (e) { + Logger.error('设置自定义日期范围失败: $e'); + } finally { + isLoading.value = false; + } + } + + String formatDate(DateTime d) { + final y = d.year.toString(); + final m = d.month.toString().padLeft(2, '0'); + final day = d.day.toString().padLeft(2, '0'); + return '$y-$m-$day'; + } +} diff --git a/lib/modules/usage/controllers/translation_cost_controller.dart b/lib/modules/usage/controllers/translation_cost_controller.dart new file mode 100644 index 000000000..934a709ec --- /dev/null +++ b/lib/modules/usage/controllers/translation_cost_controller.dart @@ -0,0 +1,120 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import '../../../data/services/usage_stats_service.dart'; +import '../../../core/utils/logger.dart'; + +class TranslationCostController extends GetxController { + final UsageStatsService _usageService = Get.find(); + final RxMap costConfig = {}.obs; + final RxDouble dailyCostThreshold = 10.0.obs; + final RxDouble monthlyCostThreshold = 300.0.obs; + final translationCostController = TextEditingController(); + final asrCostController = TextEditingController(); + final ttsCostController = TextEditingController(); + final aiSummaryCostController = TextEditingController(); + final dailyThresholdController = TextEditingController(); + final monthlyThresholdController = TextEditingController(); + final isLoading = false.obs; + final isSaving = false.obs; + + @override + void onInit() { + super.onInit(); + } + + @override + void onClose() { + translationCostController.dispose(); + asrCostController.dispose(); + ttsCostController.dispose(); + aiSummaryCostController.dispose(); + dailyThresholdController.dispose(); + monthlyThresholdController.dispose(); + super.onClose(); + } + + void _loadCurrentConfig() {} + + Future saveConfig() async {} + + void resetToDefault() {} + + bool _validateInputs() { + try { + final translationCost = double.parse(translationCostController.text); + final asrCost = double.parse(asrCostController.text); + final ttsCost = double.parse(ttsCostController.text); + final aiSummaryCost = double.parse(aiSummaryCostController.text); + final dailyThreshold = double.parse(dailyThresholdController.text); + final monthlyThreshold = double.parse(monthlyThresholdController.text); + if (translationCost < 0 || + asrCost < 0 || + ttsCost < 0 || + aiSummaryCost < 0) { + _showErrorSnackbar('成本不能为负数'); + return false; + } + if (dailyThreshold < 0 || monthlyThreshold < 0) { + _showErrorSnackbar('预警阈值不能为负数'); + return false; + } + if (dailyThreshold > monthlyThreshold) { + _showErrorSnackbar('日预警阈值不能大于月预警阈值'); + return false; + } + return true; + } catch (e) { + _showErrorSnackbar('请输入有效的数字'); + return false; + } + } + + double calculateEstimatedMonthlyCost() { + return 0.0; + } + + List getCostSavingTips() { + return []; + } + + void _showSuccessSnackbar(String message) { + Get.snackbar( + '成功', + message, + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.primary.withValues(alpha: 0.1), + colorText: Get.theme.colorScheme.primary, + duration: const Duration(seconds: 3), + ); + } + + void _showErrorSnackbar(String message) { + Get.snackbar( + '错误', + message, + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.error.withValues(alpha: 0.1), + colorText: Get.theme.colorScheme.error, + duration: const Duration(seconds: 3), + ); + } + + void _showInfoSnackbar(String message) { + Get.snackbar( + '提示', + message, + snackPosition: SnackPosition.TOP, + backgroundColor: Get.theme.colorScheme.surface.withValues(alpha: 0.9), + colorText: Get.theme.colorScheme.onSurface, + duration: const Duration(seconds: 3), + ); + } + + String formatCost(double cost) { + return '¥${cost.toStringAsFixed(2)}'; + } + + String formatPercentage(double percentage) { + return '${percentage.toStringAsFixed(1)}%'; + } +} diff --git a/lib/modules/translation/controllers/translation_stats_controller.dart b/lib/modules/usage/controllers/translation_stats_controller.dart similarity index 70% rename from lib/modules/translation/controllers/translation_stats_controller.dart rename to lib/modules/usage/controllers/translation_stats_controller.dart index 1e103b372..7a3266136 100644 --- a/lib/modules/translation/controllers/translation_stats_controller.dart +++ b/lib/modules/usage/controllers/translation_stats_controller.dart @@ -1,21 +1,16 @@ import 'package:get/get.dart'; -import 'package:flutter/material.dart'; -import '../../../data/services/translation_usage_service.dart'; +import '../../../data/services/usage_stats_service.dart'; import '../../../data/models/translation_usage_stats.dart'; import '../../../core/utils/logger.dart'; +import 'usage_controller.dart'; -/// 翻译统计页面控制器 class TranslationStatsController extends GetxController { - // 依赖注入 - final TranslationUsageService _usageService = - Get.find(); + final UsageStatsService _usageService = Get.find(); - // 状态变量 final isLoading = false.obs; - final selectedTabIndex = 0.obs; // 0=今日, 1=本周, 2=本月, 3=历史 - final selectedTimeRange = '今日'.obs; + final selectedTabIndex = 0.obs; + final selectedTimeRange = 'today'.tr.obs; // 今日 - // 统计数据 final Rx todayStats = TranslationUsageStats.empty('').obs; final Rx weekStats = @@ -25,15 +20,13 @@ class TranslationStatsController extends GetxController { final RxList historyStats = [].obs; - // 实时统计数据 - RealtimeUsageStats get realtimeStats => _usageService.realtimeStats.value; + RealtimeUsageStats get realtimeStats => + _usageService.realtimeTranslationStats.value; - // 成本配置 final RxMap costConfig = {}.obs; final RxDouble dailyCostThreshold = 10.0.obs; final RxDouble monthlyCostThreshold = 300.0.obs; - // 图表数据 final RxList> chartData = >[].obs; @override @@ -42,58 +35,37 @@ class TranslationStatsController extends GetxController { _initializeData(); } - /// 初始化数据 void _initializeData() { - // 监听使用服务的数据变化 - ever(_usageService.todayStats, (stats) { + ever(_usageService.todayTranslationStats, (stats) { todayStats.value = stats; _updateChartData(); }); - ever(_usageService.costConfig, (config) { - costConfig.value = config; - }); - - ever(_usageService.dailyCostThreshold, (threshold) { - dailyCostThreshold.value = threshold; - }); - - ever(_usageService.monthlyCostThreshold, (threshold) { - monthlyCostThreshold.value = threshold; - }); - - // 初始加载数据 loadAllStats(); } - /// 加载所有统计数据 Future loadAllStats() async { isLoading.value = true; try { - // 加载今日数据 - todayStats.value = _usageService.todayStats.value; + todayStats.value = _usageService.todayTranslationStats.value; - // 加载本周数据 await _loadWeekStats(); - // 加载本月数据 await _loadMonthStats(); - // 加载历史数据(最近7天) await _loadHistoryStats(); - // 更新图表数据 _updateChartData(); Logger.info('统计数据加载完成'); } catch (e) { Logger.error('加载统计数据失败: $e'); Get.snackbar( - '加载失败', - '统计数据加载失败,请稍后重试', + 'loadFailed'.tr, // 加载失败 + 'statsLoadFailedTryLater'.tr, // 统计数据加载失败,请稍后重试 snackPosition: SnackPosition.TOP, - backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1), + backgroundColor: Get.theme.colorScheme.error.withValues(alpha: 0.1), colorText: Get.theme.colorScheme.error, ); } finally { @@ -101,7 +73,6 @@ class TranslationStatsController extends GetxController { } } - /// 加载本周统计数据 Future _loadWeekStats() async { try { final now = DateTime.now(); @@ -119,7 +90,6 @@ class TranslationStatsController extends GetxController { } } - /// 加载本月统计数据 Future _loadMonthStats() async { try { final now = DateTime.now(); @@ -130,7 +100,6 @@ class TranslationStatsController extends GetxController { } } - /// 加载历史统计数据 Future _loadHistoryStats() async { try { final endDate = DateTime.now(); @@ -141,14 +110,12 @@ class TranslationStatsController extends GetxController { endDate: endDate, ); - // 按日期排序 historyStats.sort((a, b) => b.date.compareTo(a.date)); } catch (e) { Logger.error('加载历史统计失败: $e'); } } - /// 聚合统计数据 TranslationUsageStats _aggregateStats( List statsList, String period) { if (statsList.isEmpty) { @@ -164,7 +131,6 @@ class TranslationStatsController extends GetxController { int totalRecordingDuration = 0; int totalTtsPlayDuration = 0; int totalSessions = 0; - double totalCost = 0.0; final Map combinedLanguagePairUsage = {}; final Map combinedModeUsage = {}; @@ -179,15 +145,12 @@ class TranslationStatsController extends GetxController { totalRecordingDuration += stats.effectiveAudioDuration; totalTtsPlayDuration += stats.totalSessionDuration; totalSessions += stats.sessionCount; - totalCost += stats.estimatedCost; - // 合并语言对使用统计(使用兼容性getter) stats.languagePairUsage.forEach((key, value) { combinedLanguagePairUsage[key] = (combinedLanguagePairUsage[key] ?? 0) + value; }); - // 合并模式使用统计(使用兼容性getter) stats.modeUsage.forEach((key, value) { combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value; }); @@ -207,46 +170,44 @@ class TranslationStatsController extends GetxController { effectiveAudioDuration: totalRecordingDuration, totalSessionDuration: totalTtsPlayDuration, sessionCount: totalSessions, - estimatedCost: totalCost, + estimatedCost: 0.0, ); } - /// 更新图表数据 void _updateChartData() { final List> data = []; - // 根据选中的时间范围生成图表数据 switch (selectedTabIndex.value) { - case 0: // 今日 + case 0: data.add({ - 'label': '今日', + 'label': 'today'.tr, // 今日 'apiCalls': todayStats.value.totalApiCalls, - 'cost': todayStats.value.estimatedCost, + 'cost': 0.0, 'characters': todayStats.value.totalCharacterCount, }); break; - case 1: // 本周 + case 1: data.add({ - 'label': '本周', + 'label': 'thisWeek'.tr, // 本周 'apiCalls': weekStats.value.totalApiCalls, - 'cost': weekStats.value.estimatedCost, + 'cost': 0.0, 'characters': weekStats.value.totalCharacterCount, }); break; - case 2: // 本月 + case 2: data.add({ - 'label': '本月', + 'label': 'thisMonth'.tr, // 本月 'apiCalls': monthStats.value.totalApiCalls, - 'cost': monthStats.value.estimatedCost, + 'cost': 0.0, 'characters': monthStats.value.totalCharacterCount, }); break; - case 3: // 历史 + case 3: for (final stats in historyStats.take(7)) { data.add({ 'label': '${stats.date.month}/${stats.date.day}', 'apiCalls': stats.totalApiCalls, - 'cost': stats.estimatedCost, + 'cost': 0.0, 'characters': stats.totalCharacterCount, }); } @@ -256,29 +217,27 @@ class TranslationStatsController extends GetxController { chartData.value = data; } - /// 切换时间范围 void switchTimeRange(int index) { selectedTabIndex.value = index; switch (index) { case 0: - selectedTimeRange.value = '今日'; + selectedTimeRange.value = 'today'.tr; // 今日 break; case 1: - selectedTimeRange.value = '本周'; + selectedTimeRange.value = 'thisWeek'.tr; // 本周 break; case 2: - selectedTimeRange.value = '本月'; + selectedTimeRange.value = 'thisMonth'.tr; // 本月 break; case 3: - selectedTimeRange.value = '历史'; + selectedTimeRange.value = 'history'.tr; // 历史 break; } _updateChartData(); } - /// 获取当前显示的统计数据 TranslationUsageStats get currentStats { switch (selectedTabIndex.value) { case 0: @@ -292,32 +251,28 @@ class TranslationStatsController extends GetxController { } } - /// 刷新数据 Future refreshData() async { await loadAllStats(); } - /// 清理过期数据 void cleanupOldData() { _usageService.cleanupOldData(); Get.snackbar( - '清理完成', - '已清理90天前的过期数据', + 'cleanupComplete'.tr, // 清理完成 + 'cleanedOldData'.tr, // 已清理90天前的过期数据 snackPosition: SnackPosition.TOP, - backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1), + backgroundColor: Get.theme.colorScheme.primary.withValues(alpha: 0.1), colorText: Get.theme.colorScheme.primary, ); } - /// 导出统计数据 Future exportStats() async { try { - // TODO: 实现数据导出功能 Get.snackbar( - '导出功能', - '数据导出功能正在开发中', + 'exportFeature'.tr, // 导出功能 + 'exportInDevelopment'.tr, // 数据导出功能正在开发中 snackPosition: SnackPosition.TOP, - backgroundColor: Get.theme.colorScheme.primary.withOpacity(0.1), + backgroundColor: Get.theme.colorScheme.primary.withValues(alpha: 0.1), colorText: Get.theme.colorScheme.primary, ); } catch (e) { @@ -325,27 +280,42 @@ class TranslationStatsController extends GetxController { } } - /// 格式化时长显示 String formatDuration(int seconds) { if (seconds < 60) { - return '${seconds}秒'; + return '$seconds${'seconds'.tr}'; // 秒 } else if (seconds < 3600) { final minutes = seconds ~/ 60; final remainingSeconds = seconds % 60; - return '${minutes}分${remainingSeconds}秒'; + return '$minutes${'minutes'.tr}$remainingSeconds${'seconds'.tr}'; // 分钟秒 } else { final hours = seconds ~/ 3600; final minutes = (seconds % 3600) ~/ 60; - return '${hours}小时${minutes}分'; + return '$hours${'hours'.tr}$minutes${'minutes'.tr}'; // 小时分钟 } } - /// 格式化成本显示 String formatCost(double cost) { return '¥${cost.toStringAsFixed(2)}'; } - /// 格式化数字显示 + int _titleTapCount = 0; + int _lastTitleTapTs = 0; + + void onTitleTap() { + final now = DateTime.now().millisecondsSinceEpoch; + if (now - _lastTitleTapTs <= 800) { + _titleTapCount++; + } else { + _titleTapCount = 1; + } + _lastTitleTapTs = now; + if (_titleTapCount >= 3) { + _titleTapCount = 0; + final uc = Get.find(); + uc.toggleAiChat(!uc.showAiChat.value); + } + } + String formatNumber(int number) { if (number < 1000) { return number.toString(); diff --git a/lib/modules/usage/controllers/usage_controller.dart b/lib/modules/usage/controllers/usage_controller.dart new file mode 100644 index 000000000..7407d5e09 --- /dev/null +++ b/lib/modules/usage/controllers/usage_controller.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../views/translation_stats_view.dart'; +import '../views/meeting_stats_view.dart'; +import '../views/aichat_stats_view.dart'; + +class UsageController extends GetxController { + late PageController pageController; + final RxInt pageIndex = 0.obs; + + final RxBool showAiChat = false.obs; + + List get pageList => showAiChat.value + ? const [ + AiChatStatsView(), + TranslationStatsView(), + MeetingStatsView(), + ] + : const [ + TranslationStatsView(), + MeetingStatsView(), + ]; + + @override + void onInit() { + super.onInit(); + pageController = PageController(initialPage: 0); + } + + void toggleAiChat(bool enable) { + if (showAiChat.value == enable) return; + showAiChat.value = enable; + final int maxIndex = pageList.length - 1; + if (pageIndex.value > maxIndex) { + pageIndex.value = maxIndex; + pageController.jumpToPage(maxIndex); + } else { + pageController.jumpToPage(pageIndex.value); + } + } +} diff --git a/lib/modules/usage/views/aichat_stats_view.dart b/lib/modules/usage/views/aichat_stats_view.dart new file mode 100644 index 000000000..728bc180f --- /dev/null +++ b/lib/modules/usage/views/aichat_stats_view.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import '../../../core/theme/app_colors.dart'; +import '../controllers/aichat_stats_controller.dart'; +import '../../../data/models/user_Info.dart'; + +class AiChatStatsView extends GetView { + const AiChatStatsView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: _buildBody(isDarkMode), + ); + } + + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + leading: IconButton( + icon: Icon(Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, size: 20.sp), + onPressed: () => Get.back(), + ), + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.white, + elevation: 0.5, + title: Text( + 'aiChat'.tr, // AI聊天 + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black), + ), + centerTitle: true, + ); + } + + Widget _buildBody(bool isDarkMode) { + return Column( + children: [_buildIntegralRow(isDarkMode)], + ); + } + + Widget _buildIntegralRow(bool isDarkMode) { + return Obx(() { + final _ = User.refreshTick.value; + final u = User.instance; + return Padding( + padding: EdgeInsets.all(16.w), + child: Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2)), + ], + ), + child: Row( + children: [ + Icon(Icons.chat_bubble_outline, + color: AppColors.primary, size: 20.sp), + SizedBox(width: 8.w), + Expanded( + child: Text( + 'remainingQuota'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black), + ), + ), + Text( + '${u.aichatintegral}/${u.aichattotalintegral}', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black), + ), + ], + ), + ), + ); + }); + } +} diff --git a/lib/modules/usage/views/meeting_stats_view.dart b/lib/modules/usage/views/meeting_stats_view.dart new file mode 100644 index 000000000..0702a4aa1 --- /dev/null +++ b/lib/modules/usage/views/meeting_stats_view.dart @@ -0,0 +1,266 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import '../../../core/theme/app_colors.dart'; +import '../controllers/meeting_stats_controller.dart'; +import '../../../data/models/user_Info.dart'; +import '../../../data/models/meeting_usage_stats.dart'; + +class MeetingStatsView extends GetView { + const MeetingStatsView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], + appBar: _buildAppBar(context, isDarkMode), + body: Obx(() => controller.isLoading.value + ? const Center(child: CircularProgressIndicator()) + : _buildBody(isDarkMode)), + ); + } + + PreferredSizeWidget _buildAppBar(BuildContext context, bool isDarkMode) { + return AppBar( + leading: IconButton( + icon: Icon(Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, size: 20.sp), + onPressed: () => Get.back(), + ), + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.white, + elevation: 0.5, + title: Text( + 'meetingStats'.tr, // 会议统计 + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black), + ), + centerTitle: true, + actions: [ + IconButton( + icon: Icon(Icons.date_range, + color: isDarkMode ? Colors.white : Colors.black), + onPressed: () async { + final now = DateTime.now(); + final initialStart = controller.rangeStart.value ?? + DateTime(now.year, now.month, now.day); + final initialEnd = controller.rangeEnd.value ?? + DateTime(now.year, now.month, now.day); + final picked = await showDateRangePicker( + context: context, + firstDate: DateTime(now.year - 1), + lastDate: DateTime(now.year + 1), + initialDateRange: + DateTimeRange(start: initialStart, end: initialEnd), + ); + if (picked != null) { + await controller.setCustomRangeDates(picked.start, picked.end); + } + }, + ), + IconButton( + icon: Icon(Icons.refresh, + color: isDarkMode ? Colors.white : Colors.black), + onPressed: controller.refreshData, + ), + ], + ); + } + + Widget _buildBody(bool isDarkMode) { + final hasRange = controller.rangeStart.value != null && + controller.rangeEnd.value != null; + final historyTitle = hasRange + ? "${'history'.tr} (${controller.formatDate(controller.rangeStart.value!)} ~ ${controller.formatDate(controller.rangeEnd.value!)})" + : 'history'.tr; + return Column( + children: [ + _buildIntegralRow(isDarkMode), + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + _buildOverviewCard( + 'today'.tr, // 今日 + isDarkMode, + controller.todayStats.value, + controller.todayStorageStats.value, + ), + SizedBox(height: 16.h), + _buildOverviewCard( + historyTitle, + isDarkMode, + controller.rangeStats.value, + controller.rangeStorageStats.value, + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildIntegralRow(bool isDarkMode) { + return Obx(() { + final _ = User.refreshTick.value; + final u = User.instance; + return Padding( + padding: EdgeInsets.all(16.w), + child: Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2)), + ], + ), + child: Row( + children: [ + Icon(Icons.people, color: AppColors.primary, size: 20.sp), + SizedBox(width: 8.w), + Expanded( + child: Text( + 'remainingQuota'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black), + ), + ), + Text( + '${u.meetintegral}/${u.meettotalintegral}', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black), + ), + ], + ), + ), + ); + }); + } + + Widget _buildOverviewCard( + String title, + bool isDarkMode, + MeetingAsrHistoryStats asrStats, + MeetingStorageHistoryStats storageStats, + ) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.dashboard, color: AppColors.primary, size: 20.sp), + SizedBox(width: 8.w), + Text( + '$title${'overview'.tr}', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black), + ), + ], + ), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: _buildStatItem( + 'asrApiCalls'.tr, // ASR调用次数 + controller.formatNumber(asrStats.allAsrCalls), + Icons.api, + isDarkMode)), + Expanded( + child: _buildStatItem( + 'totalAudioDuration'.tr, // 音频总时长 + controller.formatDuration(asrStats.allAudioSeconds), + Icons.mic, + isDarkMode)), + ], + ), + SizedBox(height: 12.h), + Row( + children: [ + Expanded( + child: _buildStatItem( + 'audioFileSize'.tr, // 音频文件大小 + controller.formatBytes(asrStats.allAudioFileBytes), + Icons.folder, + isDarkMode)), + Expanded( + child: _buildStatItem( + 'transcriptBytes'.tr, // 转写字节 + controller.formatNumber(asrStats.allTranscriptBytes), + Icons.text_snippet, + isDarkMode)), + ], + ), + SizedBox(height: 12.h), + Row( + children: [ + Expanded( + child: _buildStatItem( + 'storageUploads'.tr, // 对象存储上传次数 + controller.formatNumber(storageStats.allStorageUploads), + Icons.cloud_upload, + isDarkMode)), + Expanded( + child: _buildStatItem( + 'storageBytes'.tr, // 对象存储总大小 + controller.formatBytes(storageStats.allStorageBytes), + Icons.cloud, + isDarkMode)), + ], + ), + ], + ), + ); + } + + Widget _buildStatItem( + String label, String value, IconData icon, bool isDarkMode) { + return Column( + children: [ + Icon(icon, color: AppColors.primary, size: 24.sp), + SizedBox(height: 8.h), + Text( + value, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black), + ), + SizedBox(height: 4.h), + Text( + label, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54), + ), + ], + ); + } +} diff --git a/lib/modules/translation/views/translation_stats_view.dart b/lib/modules/usage/views/translation_stats_view.dart similarity index 79% rename from lib/modules/translation/views/translation_stats_view.dart rename to lib/modules/usage/views/translation_stats_view.dart index 271bdeb31..3600000b0 100644 --- a/lib/modules/translation/views/translation_stats_view.dart +++ b/lib/modules/usage/views/translation_stats_view.dart @@ -3,8 +3,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../controllers/translation_stats_controller.dart'; import '../../../core/theme/app_colors.dart'; +import '../../../data/models/user_Info.dart'; -/// 翻译统计页面 class TranslationStatsView extends GetView { const TranslationStatsView({Key? key}) : super(key: key); @@ -21,7 +21,6 @@ class TranslationStatsView extends GetView { ); } - /// 构建应用栏 PreferredSizeWidget _buildAppBar(bool isDarkMode) { return AppBar( leading: IconButton( @@ -34,17 +33,19 @@ class TranslationStatsView extends GetView { ), backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.white, elevation: 0.5, - title: Text( - '翻译统计', // translationStats - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, + title: GestureDetector( + onTap: controller.onTitleTap, + child: Text( + 'translationStats'.tr, // + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), ), ), centerTitle: true, actions: [ - // 刷新按钮 IconButton( icon: Icon( Icons.refresh, @@ -52,73 +53,63 @@ class TranslationStatsView extends GetView { ), onPressed: controller.refreshData, ), - // 更多操作菜单 - PopupMenuButton( - icon: Icon( - Icons.more_vert, - color: isDarkMode ? Colors.white : Colors.black, - ), - onSelected: (value) { - switch (value) { - case 'cleanup': - controller.cleanupOldData(); - break; - case 'export': - controller.exportStats(); - break; - } - }, - itemBuilder: (context) => [ - const PopupMenuItem( - value: 'cleanup', - child: Text('清理过期数据'), - ), - const PopupMenuItem( - value: 'export', - child: Text('导出数据'), - ), - ], - ), + // PopupMenuButton( + // icon: Icon( + // Icons.more_vert, + // color: isDarkMode ? Colors.white : Colors.black, + // ), + // onSelected: (value) { + // switch (value) { + // case 'cleanup': + // controller.cleanupOldData(); + // break; + // // case 'export': + // // controller.exportStats(); + // // break; + // } + // }, + // itemBuilder: (context) => const [ + // PopupMenuItem( + // value: 'cleanup', + // child: Text('清理过期数据'), + // ), + // // PopupMenuItem( + // // value: 'export', + // // child: Text('导出数据'), + // // ), + // ], + // ), ], ); } - /// 构建加载组件 Widget _buildLoadingWidget() { return const Center( child: CircularProgressIndicator(), ); } - /// 构建主体内容 Widget _buildBody(bool isDarkMode) { return Column( children: [ - // 时间范围选择器 + _buildIntegralRow(isDarkMode), _buildTimeRangeSelector(isDarkMode), - // 统计内容 Expanded( child: SingleChildScrollView( padding: EdgeInsets.all(16.w), child: Column( children: [ - // 实时统计卡片 + // _buildIntegralCard(isDarkMode), + // SizedBox(height: 16.h), if (controller.selectedTabIndex.value == 0) _buildRealtimeStatsCard(isDarkMode), SizedBox(height: 16.h), - // 概览统计卡片 _buildOverviewCard(isDarkMode), SizedBox(height: 16.h), - // API调用统计 _buildApiCallsCard(isDarkMode), SizedBox(height: 16.h), - // 数据量统计 _buildDataVolumeCard(isDarkMode), SizedBox(height: 16.h), - // 成本统计 - _buildCostCard(isDarkMode), - SizedBox(height: 16.h), - // 使用行为统计 _buildUsageBehaviorCard(isDarkMode), ], ), @@ -128,7 +119,51 @@ class TranslationStatsView extends GetView { ); } - /// 构建时间范围选择器 + Widget _buildIntegralRow(bool isDarkMode) { + return Obx(() { + final _ = User.refreshTick.value; + final u = User.instance; + return Padding( + padding: EdgeInsets.all(16.w), + child: Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2)), + ], + ), + child: Row( + children: [ + Icon(Icons.translate, color: AppColors.primary, size: 20.sp), + SizedBox(width: 8.w), + Expanded( + child: Text( + 'remainingQuota'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black), + ), + ), + Text( + '${u.tradeintegral}/${u.tradetotalintegral}', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black), + ), + ], + ), + ), + ); + }); + } + Widget _buildTimeRangeSelector(bool isDarkMode) { return Container( margin: EdgeInsets.all(16.w), @@ -137,7 +172,7 @@ class TranslationStatsView extends GetView { borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -145,16 +180,15 @@ class TranslationStatsView extends GetView { ), child: Obx(() => Row( children: [ - _buildTimeRangeTab('今日', 0, isDarkMode), - _buildTimeRangeTab('本周', 1, isDarkMode), - _buildTimeRangeTab('本月', 2, isDarkMode), - _buildTimeRangeTab('历史', 3, isDarkMode), + _buildTimeRangeTab('today'.tr, 0, isDarkMode), // 今日 + _buildTimeRangeTab('thisWeek'.tr, 1, isDarkMode), // 本周 + _buildTimeRangeTab('thisMonth'.tr, 2, isDarkMode), // 本月 + _buildTimeRangeTab('history'.tr, 3, isDarkMode), // 历史 ], )), ); } - /// 构建时间范围标签 Widget _buildTimeRangeTab(String title, int index, bool isDarkMode) { final isSelected = controller.selectedTabIndex.value == index; @@ -185,7 +219,6 @@ class TranslationStatsView extends GetView { ); } - /// 构建实时统计卡片 Widget _buildRealtimeStatsCard(bool isDarkMode) { return Container( padding: EdgeInsets.all(16.w), @@ -194,7 +227,7 @@ class TranslationStatsView extends GetView { borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -212,7 +245,7 @@ class TranslationStatsView extends GetView { ), SizedBox(width: 8.w), Text( - '当前会话统计', + 'currentSessionStats'.tr, // 当前会话统计 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -226,7 +259,7 @@ class TranslationStatsView extends GetView { children: [ Expanded( child: _buildStatItem( - '翻译次数', + 'translationCount'.tr, // 翻译次数 controller.realtimeStats.translationCount.toString(), Icons.translate, isDarkMode, @@ -234,8 +267,8 @@ class TranslationStatsView extends GetView { ), Expanded( child: _buildStatItem( - '会话时长', - '${controller.realtimeStats.sessionDurationMinutes}分钟', + 'sessionDuration'.tr, // 会话时长 + '${controller.realtimeStats.sessionDurationMinutes}${'minutes'.tr}', Icons.timer, isDarkMode, ), @@ -247,7 +280,7 @@ class TranslationStatsView extends GetView { children: [ Expanded( child: _buildStatItem( - '字符数', + 'characters'.tr, // 字符数 controller.formatNumber( controller.realtimeStats.sourceCharacters + controller.realtimeStats.targetCharacters, @@ -258,7 +291,7 @@ class TranslationStatsView extends GetView { ), Expanded( child: _buildStatItem( - '录音时长', + 'recordingDuration'.tr, // 录音时长 controller.formatDuration( controller.realtimeStats.recordingDuration), Icons.mic, @@ -272,7 +305,6 @@ class TranslationStatsView extends GetView { ); } - /// 构建概览卡片 Widget _buildOverviewCard(bool isDarkMode) { return Container( padding: EdgeInsets.all(16.w), @@ -281,7 +313,7 @@ class TranslationStatsView extends GetView { borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -299,7 +331,7 @@ class TranslationStatsView extends GetView { ), SizedBox(width: 8.w), Obx(() => Text( - '${controller.selectedTimeRange.value}概览', + '${controller.selectedTimeRange.value}${'overview'.tr}', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -313,22 +345,13 @@ class TranslationStatsView extends GetView { children: [ Expanded( child: _buildStatItem( - '总调用次数', + 'totalApiCalls'.tr, // 总调用次数 controller .formatNumber(controller.currentStats.totalApiCalls), Icons.api, isDarkMode, ), ), - Expanded( - child: _buildStatItem( - '预估成本', - controller - .formatCost(controller.currentStats.estimatedCost), - Icons.attach_money, - isDarkMode, - ), - ), ], )), SizedBox(height: 12.h), @@ -336,7 +359,7 @@ class TranslationStatsView extends GetView { children: [ Expanded( child: _buildStatItem( - '会话数量', + 'sessionCount'.tr, // 会话数量 controller.currentStats.sessionCount.toString(), Icons.chat, isDarkMode, @@ -344,7 +367,7 @@ class TranslationStatsView extends GetView { ), Expanded( child: _buildStatItem( - '总字符数', + 'totalCharacters'.tr, // 总字符数 controller.formatNumber( controller.currentStats.totalCharacterCount), Icons.text_snippet, @@ -358,7 +381,6 @@ class TranslationStatsView extends GetView { ); } - /// 构建统计项 Widget _buildStatItem( String label, String value, IconData icon, bool isDarkMode) { return Column( @@ -389,7 +411,6 @@ class TranslationStatsView extends GetView { ); } - /// 构建API调用统计卡片 Widget _buildApiCallsCard(bool isDarkMode) { return Container( padding: EdgeInsets.all(16.w), @@ -398,7 +419,7 @@ class TranslationStatsView extends GetView { borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -416,7 +437,7 @@ class TranslationStatsView extends GetView { ), SizedBox(width: 8.w), Text( - 'API调用统计', + 'apiCallStats'.tr, // API调用统计 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -429,19 +450,25 @@ class TranslationStatsView extends GetView { Obx(() => Column( children: [ _buildApiCallRow( - '翻译API', + 'translationApi'.tr, // 翻译API controller.currentStats.translationApiCalls, Icons.translate, isDarkMode), SizedBox(height: 12.h), - _buildApiCallRow('语音识别', controller.currentStats.asrApiCalls, - Icons.mic, isDarkMode), + _buildApiCallRow( + 'asr'.tr, + controller.currentStats.asrApiCalls, + Icons.mic, + isDarkMode), SizedBox(height: 12.h), - _buildApiCallRow('语音合成', controller.currentStats.ttsApiCalls, - Icons.volume_up, isDarkMode), + _buildApiCallRow( + 'tts'.tr, + controller.currentStats.ttsApiCalls, + Icons.volume_up, + isDarkMode), SizedBox(height: 12.h), _buildApiCallRow( - 'AI摘要', + 'aiSummary'.tr, // AI总结 controller.currentStats.aiSummaryApiCalls, Icons.summarize, isDarkMode), @@ -452,7 +479,6 @@ class TranslationStatsView extends GetView { ); } - /// 构建API调用行 Widget _buildApiCallRow( String label, int count, IconData icon, bool isDarkMode) { return Row( @@ -484,7 +510,6 @@ class TranslationStatsView extends GetView { ); } - /// 构建数据量统计卡片 Widget _buildDataVolumeCard(bool isDarkMode) { return Container( padding: EdgeInsets.all(16.w), @@ -493,7 +518,7 @@ class TranslationStatsView extends GetView { borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -511,7 +536,7 @@ class TranslationStatsView extends GetView { ), SizedBox(width: 8.w), Text( - '数据量统计', + 'dataVolumeStats'.tr, // 数据量统计 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -527,7 +552,7 @@ class TranslationStatsView extends GetView { children: [ Expanded( child: _buildStatItem( - '源文本字符', + 'sourceTextCharacters'.tr, // 源文本字符 controller.formatNumber( controller.currentStats.sourceTextCharacters), Icons.text_fields, @@ -536,7 +561,7 @@ class TranslationStatsView extends GetView { ), Expanded( child: _buildStatItem( - 'TTS文本字符', + 'ttsTextCharacters'.tr, // TTS文本字符 controller.formatNumber( controller.currentStats.ttsTextCharacters), Icons.translate, @@ -550,7 +575,7 @@ class TranslationStatsView extends GetView { children: [ Expanded( child: _buildStatItem( - '有效音频时长', + 'effectiveAudioDuration'.tr, // 有效音频时长 controller.formatDuration( controller.currentStats.effectiveAudioDuration), Icons.mic, @@ -559,7 +584,7 @@ class TranslationStatsView extends GetView { ), Expanded( child: _buildStatItem( - '总会话时长', + 'totalSessionDuration'.tr, // 总会话时长 controller.formatDuration( controller.currentStats.totalSessionDuration), Icons.volume_up, @@ -575,7 +600,6 @@ class TranslationStatsView extends GetView { ); } - /// 构建成本统计卡片 Widget _buildCostCard(bool isDarkMode) { return Container( padding: EdgeInsets.all(16.w), @@ -584,7 +608,7 @@ class TranslationStatsView extends GetView { borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -602,7 +626,7 @@ class TranslationStatsView extends GetView { ), SizedBox(width: 8.w), Text( - '成本统计', + 'costStats'.tr, // 成本统计 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -614,18 +638,17 @@ class TranslationStatsView extends GetView { SizedBox(height: 16.h), Obx(() => Column( children: [ - // 当前成本 Container( padding: EdgeInsets.all(12.w), decoration: BoxDecoration( - color: AppColors.primary.withOpacity(0.1), + color: AppColors.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8.r), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '${controller.selectedTimeRange.value}预估成本', + '${controller.selectedTimeRange.value}${'estimatedCost'.tr}', style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w500, @@ -645,12 +668,11 @@ class TranslationStatsView extends GetView { ), ), SizedBox(height: 12.h), - // 预警阈值 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '日成本预警阈值', + 'dailyCostThreshold'.tr, // 日成本预警阈值 style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.white70 : Colors.black54, @@ -671,7 +693,7 @@ class TranslationStatsView extends GetView { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '月成本预警阈值', + 'monthlyCostThreshold'.tr, // 月成本预警阈值 style: TextStyle( fontSize: 12.sp, color: isDarkMode ? Colors.white70 : Colors.black54, @@ -694,7 +716,6 @@ class TranslationStatsView extends GetView { ); } - /// 构建使用行为统计卡片 Widget _buildUsageBehaviorCard(bool isDarkMode) { return Container( padding: EdgeInsets.all(16.w), @@ -703,7 +724,7 @@ class TranslationStatsView extends GetView { borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), @@ -721,7 +742,7 @@ class TranslationStatsView extends GetView { ), SizedBox(width: 8.w), Text( - '使用行为统计', + 'usageBehaviorStats'.tr, // 使用行为统计 style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -733,17 +754,15 @@ class TranslationStatsView extends GetView { SizedBox(height: 16.h), Obx(() => Column( children: [ - // 最常用语言对 _buildUsageBehaviorRow( - '最常用语言对', + 'mostUsedLanguagePair'.tr, // 最常用语言对 controller.currentStats.mostUsedLanguagePair, Icons.language, isDarkMode, ), SizedBox(height: 12.h), - // 最常用翻译模式 _buildUsageBehaviorRow( - '最常用模式', + 'mostUsedMode'.tr, // 最常用模式 controller.currentStats.mostUsedMode, Icons.settings, isDarkMode, @@ -755,7 +774,6 @@ class TranslationStatsView extends GetView { ); } - /// 构建使用行为行 Widget _buildUsageBehaviorRow( String label, String value, IconData icon, bool isDarkMode) { return Row( diff --git a/lib/modules/usage/views/usage_view.dart b/lib/modules/usage/views/usage_view.dart new file mode 100644 index 000000000..7bf1c8ead --- /dev/null +++ b/lib/modules/usage/views/usage_view.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import '../../../core/theme/app_colors.dart'; +import '../controllers/usage_controller.dart'; + +class UsageView extends GetView { + const UsageView({super.key}); + + // 2. 颜色配置集中管理 + Color _getBgColor(bool isDark) => + isDark ? Colors.black : const Color(0xFFF9FAFB); + Color _getUnselectedColor(bool isDark) => + isDark ? Colors.grey[500]! : const Color(0xFF6B7280); + Color _getSelectedColor(bool isDark) => + isDark ? Colors.blue[200]! : const Color(0xFF3B82F6); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], + body: Obx(() => PageView( + physics: const NeverScrollableScrollPhysics(), + controller: controller.pageController, + children: controller.pageList, + )), + bottomNavigationBar: Obx( + () { + final currentIndex = controller.pageIndex.value; + + return Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: isDarkMode ? Colors.grey[800]! : Colors.grey[200]!, + width: 0.5, + )), + ), + // 3. 添加安全区域适配(针对刘海屏) + child: BottomNavigationBar( + // 4. 使用生成的导航项 + items: (controller.showAiChat.value + ? const [ + _NavItem(Icons.chat_bubble_outline, 'aiChat'), + _NavItem(Icons.translate, 'interpretation'), + _NavItem(Icons.people, 'meeting'), + ] + : const [ + _NavItem(Icons.translate, 'interpretation'), + _NavItem(Icons.people, 'meeting'), + ]) + .map((item) => BottomNavigationBarItem( + icon: Icon(item.icon), + activeIcon: Icon(item.icon, + color: _getSelectedColor(isDarkMode)), // 5. 激活态图标 + label: item.label.tr, // 统一处理国际化 + )) + .toList(), + + backgroundColor: _getBgColor(isDarkMode), + currentIndex: currentIndex, + unselectedItemColor: _getUnselectedColor(isDarkMode), + selectedItemColor: _getSelectedColor(isDarkMode), + unselectedFontSize: 10.sp, + selectedFontSize: 10.sp, + + // 6. 添加动画过渡效果 + onTap: (index) => _onItemTapped(index, currentIndex), + + type: BottomNavigationBarType.fixed, + // 7. 增加图标和文字的间距 + iconSize: 22.w, // 响应式图标尺寸 + selectedLabelStyle: TextStyle(height: 1.5.sp), // 文字垂直间距 + unselectedLabelStyle: TextStyle(height: 1.5.sp), + ), + ); + }, + ), + ); + } + + // 9. 抽离点击逻辑 + void _onItemTapped(int index, int currentIndex) { + if (index == currentIndex) return; + + // 10. 添加页面切换动画 + controller.pageController.animateToPage( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + controller.pageIndex.value = index; + } +} + +// 11. 使用数据类管理导航项配置 +class _NavItem { + final IconData icon; + final String label; + + const _NavItem(this.icon, this.label); +} diff --git a/lib/modules/voice_replication/bindings/voice_replication_binding.dart b/lib/modules/voice_replication/bindings/voice_replication_binding.dart new file mode 100644 index 000000000..d52da42aa --- /dev/null +++ b/lib/modules/voice_replication/bindings/voice_replication_binding.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; + +import '../controllers/voice_replication_controller.dart'; + +class VoiceReplicationBinding extends Bindings { + @override + void dependencies() { + // 注册控制器 + Get.lazyPut(() => VoiceReplicationController()); + } +} diff --git a/lib/modules/voice_replication/controllers/voice_replication_controller.dart b/lib/modules/voice_replication/controllers/voice_replication_controller.dart new file mode 100644 index 000000000..686da8313 --- /dev/null +++ b/lib/modules/voice_replication/controllers/voice_replication_controller.dart @@ -0,0 +1,39 @@ +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +import '../../../data/services/tts_service.dart'; + +class VoiceReplicationController extends GetxController { + final TtsService _ttsService = Get.find(); + + final GetStorage _storage = GetStorage(); + + RxBool isReplicating = false.obs; + + RxString speakerProfileId = ''.obs; + + RxBool isAgent = false.obs; + RxBool isSimultaneous = false.obs; + RxBool isCall = false.obs; + RxBool isFaceToFace = false.obs; + + @override + void onInit() { + super.onInit(); + _getDefaultData(); + } + + void _getDefaultData() { + isReplicating.value = _storage.read('open_voice_replication') ?? false; + speakerProfileId.value = _storage.read('speakerProfileId') ?? ''; + } + + void enableVoiceFlocking() { + if (speakerProfileId.value.isNotEmpty) { + _ttsService.setVoiceFlocking( + isReplicating.value ? speakerProfileId.value : '', + ); + _storage.write('open_voice_replication', isReplicating.value); + } + } +} diff --git a/lib/modules/voice_replication/views/recorder_bottom_sheet.dart b/lib/modules/voice_replication/views/recorder_bottom_sheet.dart new file mode 100644 index 000000000..ea8475343 --- /dev/null +++ b/lib/modules/voice_replication/views/recorder_bottom_sheet.dart @@ -0,0 +1,341 @@ +import 'dart:io'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:record/record.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../core/utils/permission_util.dart'; +import '../../../data/models/appconfig.dart'; +import '../controllers/voice_replication_controller.dart'; + +class RecorderBottomSheet extends StatefulWidget { + const RecorderBottomSheet({super.key}); + + @override + State createState() => _RecorderBottomSheetState(); +} + +class _RecorderBottomSheetState extends State { + final _controller = Get.find(); + + final GetStorage _storage = GetStorage(); + final _record = AudioRecorder(); + + bool _isRecording = false; + + String _resourceKey = ''; + String _resourceRegion = ''; + String _jessicaConsentId = ''; + + String _voiceTalentName = ''; + String _jessicaPersonalVoiceId = ''; + + String _path = ''; + + @override + void initState() { + super.initState(); + _getDefaultData(); + } + + @override + void dispose() { + _record.dispose(); + super.dispose(); + } + + void _getDefaultData() async { + _resourceKey = AppConfig.env('AZURE_SPEECH_KEY') ?? ''; + _resourceRegion = AppConfig.env('AZURE_SPEECH_REGION') ?? 'eastasia'; + _jessicaConsentId = const Uuid().v4(); + Map? userInfo = _storage.read('user_info'); + if (userInfo != null) { + _voiceTalentName = userInfo['user']['name'] ?? ''; + _jessicaPersonalVoiceId = userInfo['user']['uid'] ?? ''; + } + } + + void _startRecording() async { + setState(() { + _isRecording = true; + }); + await PermissionUtil.instance.requestPermission( + permissionType: Permission.microphone, + permissionName: 'microphonePermission'.tr, + explanationText: 'microphonePermissionExplanation'.tr, + permanentDenialText: 'microphonePermissionPermanentDenial'.tr, + ); + final directory = await getApplicationDocumentsDirectory(); + final recordDir = Directory('${directory.path}/VoiceReplication'); + if (!await recordDir.exists()) { + await recordDir.create(recursive: true); + } + _path = + '${directory.path}/VoiceReplication/recording_${DateTime.now().millisecondsSinceEpoch}.wav'; + await _record.start( + const RecordConfig( + encoder: AudioEncoder.wav, + sampleRate: 24000, + numChannels: 1, + ), + path: _path, + ); + } + + void _completeRecorder() async { + EasyLoading.show(status: '正在创建个人语音...'); + try { + await _record.stop(); + await _consents(); + await _personalvoices(); + } catch (e) { + Get.snackbar('error'.tr, e.toString(), snackPosition: SnackPosition.TOP); + } + EasyLoading.dismiss(); + } + + Future _consents() async { + final url = Uri.parse( + 'https://$_resourceRegion.api.cognitive.microsoft.com/customvoice/consents/$_jessicaConsentId?api-version=2024-02-01-preview'); + final request = http.MultipartRequest('POST', url); + request.headers['Ocp-Apim-Subscription-Key'] = _resourceKey; + + request.fields['description'] = 'Consent for $_voiceTalentName voice'; + request.fields['projectId'] = 'Voitrans'; + request.fields['voiceTalentName'] = _voiceTalentName; + request.fields['companyName'] = '深圳市云衍智能科技有限公司'; + request.fields['locale'] = 'zh-CN'; + + request.files.add(await http.MultipartFile.fromPath('audiodata', _path)); + + final streamed = await request.send(); + final response = await http.Response.fromStream(streamed); + if (streamed.statusCode >= 200 && streamed.statusCode < 300) { + final bodyText = utf8.decode(response.bodyBytes, allowMalformed: true); + if (bodyText.isNotEmpty) { + final decoded = jsonDecode(bodyText); + + String status = decoded['status'] ?? 'NotStarted'; + final operationLocation = response.headers['operation-location']; + int retryCount = 0; + + while (status != 'Succeeded' && status != 'Failed' && retryCount < 30) { + await Future.delayed(const Duration(seconds: 1)); + if (operationLocation != null && operationLocation.isNotEmpty) { + status = await _operations(operationLocation); + } else { + final consentUrl = + 'https://$_resourceRegion.api.cognitive.microsoft.com/customvoice/consents/$_jessicaConsentId?api-version=2024-02-01-preview'; + status = await _operations(consentUrl); + } + retryCount++; + } + + if (status != 'Succeeded') { + throw Exception('Consent validation failed or timed out: $status'); + } + } + } else { + throw Exception('Failed to create consent: ${response.statusCode}'); + } + } + + Future _operations(String operationUrl) async { + String status = 'NotStarted'; + + final url = Uri.parse(operationUrl); + final response = await http.get(url, headers: { + 'Ocp-Apim-Subscription-Key': _resourceKey, + }); + if (response.statusCode == 200) { + final bodyText = utf8.decode(response.bodyBytes, allowMalformed: true); + if (bodyText.isNotEmpty) { + final decoded = jsonDecode(bodyText); + status = decoded['status'] ?? status; + } + } + return status; + } + + Future _personalvoices() async { + final url = Uri.parse( + 'https://$_resourceRegion.api.cognitive.microsoft.com/customvoice/personalvoices/$_jessicaPersonalVoiceId?api-version=2024-02-01-preview'); + final request = http.MultipartRequest('POST', url); + request.headers['Ocp-Apim-Subscription-Key'] = _resourceKey; + + request.fields['projectId'] = 'Voitrans'; + request.fields['consentId'] = _jessicaConsentId; + + request.files.add(await http.MultipartFile.fromPath('audiodata', _path)); + + final streamed = await request.send(); + final response = await http.Response.fromStream(streamed); + if (streamed.statusCode >= 200 && streamed.statusCode < 300) { + final bodyText = utf8.decode(response.bodyBytes, allowMalformed: true); + if (bodyText.isNotEmpty) { + final decoded = jsonDecode(bodyText); + _controller.speakerProfileId.value = decoded['speakerProfileId']; + _storage.write('open_voice_replication', true); + _storage.write('speakerProfileId', decoded['speakerProfileId']); + } + } else { + throw Exception( + 'Failed to create personal voice: ${response.statusCode}'); + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '同意并朗读使用个人语音', + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + fontWeight: FontWeight.bold, + ), + ), + GestureDetector( + onTap: () => Get.back(), + child: Container( + width: 26.w, + height: 26.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(26.r), + ), + child: Icon( + Icons.close, + size: 18.w, + color: Colors.grey[400], + ), + ), + ) + ], + ), + ), + Divider( + height: 1, + color: Colors.grey[300], + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), + margin: EdgeInsets.symmetric(vertical: 20.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '请朗读:', + style: TextStyle( + fontSize: 14.sp, + color: Colors.black87, + ), + ), + Padding( + padding: EdgeInsets.only(top: 20.w, bottom: 50.w), + child: Text( + '我(你的姓名)确认我的声音将会被(深圳市云衍智能科技有限公司)使用于创建合成版本语音。', + style: TextStyle( + fontSize: 16.sp, + color: Colors.red, + ), + ), + ), + Text( + '例:我(李伟)确认我的声音将会被(深圳市云衍智能科技有限公司)使用于创建合成版本语音。', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey, + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 20.w), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => Get.back(), + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + 'cancel'.tr, // 取消 + style: TextStyle( + fontSize: 16.sp, + color: Colors.black, + ), + ), + ), + ), + ), + 10.horizontalSpace, + Expanded( + child: GestureDetector( + onTap: () { + if (_isRecording) { + Get.back(); + _completeRecorder(); + } else { + _startRecording(); + } + }, + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: _isRecording ? Colors.blue : Colors.red, + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + _isRecording ? 'confirm'.tr : 'startRecording'.tr, // 确认 + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/voice_replication/views/voice_replication_view.dart b/lib/modules/voice_replication/views/voice_replication_view.dart new file mode 100644 index 000000000..bb46f745a --- /dev/null +++ b/lib/modules/voice_replication/views/voice_replication_view.dart @@ -0,0 +1,282 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +import '../controllers/voice_replication_controller.dart'; +import 'recorder_bottom_sheet.dart'; + +class VoiceReplicationView extends GetView { + const VoiceReplicationView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: _buildBody(isDarkMode), + ); + } + + // 构建应用栏 + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), + ), + title: Text( + '声音复刻', + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + ); + } + + // 构建主体内容 + Widget _buildBody(bool isDarkMode) { + return ListView( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 15.w), + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), + margin: EdgeInsets.only(bottom: 15.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + Container( + width: 60.w, + height: 60.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + border: Border.all( + color: Colors.grey, + ), + ), + child: Icon( + Icons.voice_chat, + size: 40.w, + ), + ), + Padding( + padding: EdgeInsets.only(top: 20.w, bottom: 10.w), + child: Text( + '首月免费使用', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 18.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + Text( + '使用声音复刻可为你生成专属音色档案,在配音、播报与对话中保留你的声音特征,适用于视频解说、虚拟助理、无障碍朗读等场景。', + textAlign: TextAlign.center, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 14.sp, + ), + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 2.w), + margin: EdgeInsets.only(bottom: 15.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Obx( + () => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSwitch( + isDarkMode, + '开启声音复刻', + controller.isReplicating.value, + onChanged: (bool value) { + controller.isReplicating.value = value; + controller.enableVoiceFlocking(); + }, + ), + if (controller.isReplicating.value) _buildOpen(isDarkMode), + ], + ), + ), + ), + Obx( + () => controller.isReplicating.value + ? Container( + padding: + EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), + margin: EdgeInsets.only(bottom: 15.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.white, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withValues(alpha: 0.2) + : Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + _buildSwitch( + isDarkMode, + 'AI全能助手开启', + controller.isAgent.value, + onChanged: (bool value) { + controller.isAgent.value = value; + }, + ), + _buildSwitch( + isDarkMode, + '同声翻译开启', + controller.isSimultaneous.value, + onChanged: (bool value) { + controller.isSimultaneous.value = value; + }, + ), + _buildSwitch( + isDarkMode, + '通话翻译开启', + controller.isCall.value, + onChanged: (bool value) { + controller.isCall.value = value; + }, + ), + _buildSwitch( + isDarkMode, + '面对面翻译开启', + controller.isFaceToFace.value, + onChanged: (bool value) { + controller.isFaceToFace.value = value; + }, + ), + ], + ), + ) + : const SizedBox.shrink(), + ), + ], + ); + } + + Widget _buildOpen(bool isDarkMode) { + return Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: controller.speakerProfileId.value.isNotEmpty + ? Text( + '个人语音ID:\n${controller.speakerProfileId.value}', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.grey, + fontSize: 14.sp, + ), + ) + : GestureDetector( + onTap: () { + Get.bottomSheet( + const RecorderBottomSheet(), + isScrollControlled: true, + backgroundColor: + isDarkMode ? Colors.grey[800] : Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 6.w, + ), + decoration: BoxDecoration( + color: Colors.blue.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6.r), + border: Border.all( + color: Colors.blue.withValues(alpha: 0.3), + ), + ), + child: Text( + '创建个人语音', + style: TextStyle( + fontSize: 14.sp, + color: Colors.blue, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ); + } + + Widget _buildSwitch( + bool isDarkMode, + String title, + bool value, { + Function(bool)? onChanged, + }) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 2.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 16.sp, + ), + ), + Transform.scale( + scale: 0.8, + alignment: Alignment.centerRight, + child: Switch( + value: value, + onChanged: onChanged, + ), + ), + ], + ), + ); + } +} diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 816c98986..2aa1aa1c5 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -1,8 +1,9 @@ import 'package:get/get.dart'; -import 'package:qq_music/qq_music.dart'; -import '/modules/qqmusic/bindings/qqmusic_playlist_binding.dart'; -import '/modules/qqmusic/views/qqmusic_playlist_view.dart'; +import '../modules/qqmusic/bindings/qqmusic_playlist_binding.dart'; +import '../modules/qqmusic/views/qqmusic_playlist_view.dart'; +import '../modules/voice_replication/bindings/voice_replication_binding.dart'; +import '../modules/voice_replication/views/voice_replication_view.dart'; import '/modules/ble/bindings/ble_binding.dart'; import '../modules/ble/views/ble_view.dart'; @@ -35,8 +36,8 @@ import '../modules/timbre/views/timbre_view.dart'; import '../modules/translation/views/translation_view.dart'; import '../modules/translation/bindings/translation_binding.dart'; import '../modules/translation/views/translation_history_view.dart'; -import '../modules/translation/views/translation_stats_view.dart'; -import '../modules/translation/bindings/translation_stats_binding.dart'; +import '../modules/usage/views/usage_view.dart'; +import '../modules/usage/bindings/usage_binding.dart'; import '../modules/translation/bindings/translation_list_binding.dart'; import '../modules/translation/views/translation_list_view.dart'; import '../modules/translation/bindings/translation_history_binding.dart'; @@ -73,6 +74,14 @@ import '../modules/text_translation/bindings/text_translation_binding.dart'; import '../modules/text_translation/views/text_translation_view.dart'; import '../modules/picture_translation/views/picture_translation_view.dart'; import '../modules/picture_translation/bindings/picture_translation_binding.dart'; +import '../modules/goods/bindings/goodsvip_binding.dart'; +import '../modules/goods/views/goodsvip_view.dart'; +import '../modules/goods/bindings/goodstrans_binding.dart'; +import '../modules/goods/views/goodstrans_view.dart'; +import '../modules/goods/bindings/goodsmeet_binding.dart'; +import '../modules/goods/views/goodsmeet_view.dart'; +import '../modules/goods/bindings/goodsai_binding.dart'; +import '../modules/goods/views/goodsai_view.dart'; abstract class AppPages { static final pages = [ @@ -192,9 +201,9 @@ abstract class AppPages { transitionDuration: Duration(milliseconds: 250), ), GetPage( - name: Routes.translationStats, - page: () => const TranslationStatsView(), - binding: TranslationStatsBinding(), + name: Routes.usageStats, + page: () => const UsageView(), + binding: UsageBinding(), transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), @@ -240,6 +249,13 @@ abstract class AppPages { transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), + GetPage( + name: Routes.voiceReplication, + page: () => const VoiceReplicationView(), + binding: VoiceReplicationBinding(), + transition: Transition.rightToLeft, + transitionDuration: Duration(milliseconds: 250), + ), GetPage( name: Routes.settings, page: () => const SettingsView(), @@ -327,9 +343,30 @@ abstract class AppPages { transitionDuration: Duration(milliseconds: 250), ), GetPage( - name: Routes.wakeupvoice, - page: () => const WakeupVoiceView(), - binding: WakeupVoiceBinding(), + name: Routes.goodsvip, + page: () => const GoodsvipView(), + binding: GoodsvipBinding(), + transition: Transition.rightToLeft, + transitionDuration: Duration(milliseconds: 250), + ), + GetPage( + name: Routes.goodstrans, + page: () => const GoodstransView(), + binding: GoodstransBinding(), + transition: Transition.rightToLeft, + transitionDuration: Duration(milliseconds: 250), + ), + GetPage( + name: Routes.goodsmeet, + page: () => const GoodsmeetView(), + binding: GoodsmeetBinding(), + transition: Transition.rightToLeft, + transitionDuration: Duration(milliseconds: 250), + ), + GetPage( + name: Routes.goodsai, + page: () => const GoodsaiView(), + binding: GoodsaiBinding(), transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 38e65a1af..dca714c61 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -16,9 +16,11 @@ abstract class Routes { // 设备管理 static const OTA_UPGRADE = '/ota_upgrade'; // OTA升级 static const timbre = '/timbre'; // 音色设置 + static const voiceReplication = '/voice_replication'; // 声音复刻 static const settings = '/settings'; // 系统设置 static const permissions = '/permissions'; // 权限管理 static const recording = '/recording'; // 录音功能 + static const eqSettings = '/eq_settings'; // 设备设置 // 会议相关 static const meeting = '/meeting'; @@ -31,7 +33,7 @@ abstract class Routes { static const pictureTranslation = '/picture_translation'; // 图片翻译 static const textTranslation = '/text_translation'; // 文本翻译 static const translationHistory = '/translation/history'; // 翻译历史 - static const translationStats = '/translation/stats'; // 翻译统计 + static const usageStats = '/usage/stats'; // 用量统计 static const speechDemo = '/speech_demo'; // 语音演示 static const translationList = '/translation/List'; // 翻译历史 static const ftftranslation = '/ftftranslation'; // 面对面翻译 @@ -69,5 +71,10 @@ abstract class Routes { // 支付相关 static const payment = '/payment'; // 支付测试页面 - static const wakeupvoice = '/wakeupvoice'; //唤醒词管理 + + //商品购买页面 + static const goodsvip = '/goodsvip'; //vip商品页面 + static const goodstrans = '/goodstrans'; //翻译商品页面 + static const goodsmeet = '/goodsmeet'; //会议商品页面 + static const goodsai = '/goodsai'; //ai商品页面 } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index f443e8bbe..382b3ca70 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -19,6 +20,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); g_autoptr(FlPluginRegistrar) record_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); record_linux_plugin_register_with_registrar(record_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 346ccb75e..bd3f71e4d 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux flutter_secure_storage_linux + gtk record_linux url_launcher_linux window_size diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 33c38154b..961f578be 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -97,7 +97,8 @@ object AgentService : CoroutineScope { // 会话ID,用于区分不同聊天上下文 private val agentId = "default_agent" //工具调用提示 - private var callingTool = "" + private var callingToolText = "" + private var insufficientIntegralText = "" // 积分不足提示 // 服务端连接 private var serverurl = "" private var usertoken = "" @@ -134,6 +135,9 @@ object AgentService : CoroutineScope { // AI流生成相关 private var currentAiJob: Job? = null private var currsessionId: String = "" + private var currsessionAsrId: String = "" + + private var vipExptime:Int = 0 // 空闲检测相关 private var idleCheckJob: Job? = null @@ -146,6 +150,9 @@ object AgentService : CoroutineScope { private var isplaymusic = false; //是否保留最终结果 private var isKeepResult = true; + // 识别结果 + private var recognitionResult = "" + private var pendingProcessJob: Job? = null // 打断防抖相关 private var lastInterruptTime = 0L @@ -169,7 +176,7 @@ object AgentService : CoroutineScope { */ private fun initSystemPrompt() { systemPrompt = """ - 你是小言,一个亲切而温暖的全能个人助理和伴侣,拥有智能、细致而贴心的服务能力。你的任务不仅是高效处理用户的日常事务,更要陪伴用户,提供情感支持和温暖陪伴。你的能力包括: + 你是Lily,一个亲切而温暖的全能个人助理和伴侣,拥有智能、细致而贴心的服务能力。你的任务不仅是高效处理用户的日常事务,更要陪伴用户,提供情感支持和温暖陪伴。你的能力包括: 1. 日程与任务管理:温柔提醒用户的重要日程、待办事项,让用户感到安心和舒适。 2. 日常信息与建议:亲切、迅速地提供天气、新闻、生活小贴士等实用信息。 3. 个人成长陪伴:鼓励并陪伴用户实现个人目标,提出富有同理心的行动建议。 @@ -202,8 +209,10 @@ object AgentService : CoroutineScope { try { serverurl = config["serverurl"]?.toString() ?: ""; usertoken = config["usertoken"]?.toString() ?: ""; - callingTool = config["callingTool"]?.toString() ?: ""; - + callingToolText = config["callingToolText"]?.toString() ?: ""; + insufficientIntegralText = config["insufficientIntegralText"]?.toString() ?: ""; + vipExptime = config["vipexptime"]?.toString()?.toIntOrNull() ?: 0; + Log.d(TAG, "iwei------ vipexptime ${vipExptime}") // 初始化上下文和服务 this.context = appContext.applicationContext @@ -285,6 +294,20 @@ object AgentService : CoroutineScope { } } + fun applyConfigUpdate(config: Map) { + try { + serverurl = config["serverurl"]?.toString() ?: serverurl + usertoken = config["usertoken"]?.toString() ?: usertoken + callingToolText = config["callingToolText"]?.toString() ?: callingToolText + insufficientIntegralText = config["insufficientIntegralText"]?.toString() ?: insufficientIntegralText + vipExptime = config["vipexptime"]?.toString()?.toIntOrNull() ?: vipExptime; + navigationMode = config["navigationMode"]?.toString() ?: navigationMode + } catch (e: Exception) { + Log.e(TAG, "应用配置更新失败 config: $config Exception:${e.message}") + } + } + + /** * 释放所有资源 */ @@ -505,10 +528,12 @@ object AgentService : CoroutineScope { try { delay(idleSeconds * 1000L) Log.d(TAG, "空闲检测触发自动停止,isActive: $isActive, isRecognitionActive: $isRecognitionActive, isTtsSpeaking: $isTtsSpeaking, isAiStreaming: $isAiStreaming, mode: $mode") - if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming && mode != "phone_call") { + if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming) { Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode") - BleService.closeCodec() - stopRecognition() + BleService.closeCodec() + if (mode != "phone_call"){ + stopRecognition() + } sendEvent( "auto_stop", mapOf( @@ -518,10 +543,6 @@ object AgentService : CoroutineScope { ) ) } - if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming && mode == "phone_call") { - Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode") - startRecognition(false, mode) - } } catch (e: CancellationException) { // 协程被取消,正常行为 } catch (e: Exception) { @@ -620,11 +641,11 @@ object AgentService : CoroutineScope { if(isExternalActive){ isKeepResult = true } - azureAsrHelper?.startContinuousRecognition(audioSourceType) + azureAsrHelper?.startContinuousRecognition(audioSourceType, isRemoveFirstPunctuation = false) // 根据模式启动相应的空闲检测 startIdleCheckForMode(mode) - // 设置当前识别模式 + // 设置当前识别模式 currentRecognitionMode = mode Log.d(TAG, "设置语音识别模式: $mode") return true @@ -647,21 +668,25 @@ object AgentService : CoroutineScope { val success = azureAsrHelper?.setupEventListeners(object : AzureAsrHelper.ContinuousRecognizeCallback { override fun onRecognizing(sessionid:String, recognizing: String, detectedLanguage: String) { + Log.d(TAG, "liwei-------- onRecognizing: $sessionid") + pendingProcessJob?.cancel() if (recognizing.isNotEmpty()&&isKeepResult==true) { // 检测到语音,更新状态 _hasSpeechDetected.set(true) Log.d(TAG, "ai识别中重置定时器") restartIdleCheck() - - + if(currsessionAsrId.isEmpty()) { + currsessionAsrId = sessionid + } + var text = recognitionResult + recognizing sendEvent( "recognizing", mapOf( - "sessionid" to sessionid, - "text" to recognizing, + "sessionid" to currsessionAsrId, + "text" to text, "language" to detectedLanguage ) ) - + // 如果TTS正在播放或AI正在生成,检查是否需要打断 if (isTtsSpeaking || isAiStreaming) { val currentTime = System.currentTimeMillis() @@ -690,44 +715,92 @@ object AgentService : CoroutineScope { } override fun onResult(sessionid:String,result: String, detectedLanguage: String) { + Log.d(TAG, "liwei-------- onResult: $sessionid ") if (result.isNotEmpty()&&isKeepResult==true) {//加入是否结束要输出最后的结果判读 - sendEvent( - "recognition_result", mapOf( - "sessionid" to sessionid, - "text" to result, - "language" to detectedLanguage + // 保存识别结果 + if (currsessionAsrId.isEmpty()) { + currsessionAsrId = sessionid + } + recognitionResult += result + if (currentRecognitionMode == "push_to_talk") { //按住说话 + sendEvent( + "recognizing", mapOf( + "sessionid" to currsessionAsrId, + "text" to recognitionResult, + "language" to detectedLanguage + ) ) - ) - - processWithChatApi(sessionid,result) + }else{ + pendingProcessJob?.cancel() + sendEvent( + "recognizing", mapOf( + "sessionid" to currsessionAsrId, + "text" to recognitionResult, + "language" to detectedLanguage + ) + ) + pendingProcessJob = launch { + delay(1500) + if (recognitionResult.isNotEmpty()) { + sendEvent( + "recognition_result", mapOf( + "sessionid" to currsessionAsrId, + "text" to recognitionResult, + "language" to detectedLanguage + ) + ) + processWithChatApi(currsessionAsrId, recognitionResult) + recognitionResult = "" + currsessionAsrId = "" + } + } + } } - _hasSpeechDetected.set(false) restartIdleCheck() } override fun onSessionStarted(sessionid:String) { + Log.d(TAG, "liwei-------- onSessionStarted: $sessionid") sendEvent("recognition_started", mapOf("sessionid" to sessionid, "status" to "started")) // 使用模式感知的空闲检测启动方法 + recognitionResult = "" + currsessionAsrId = "" + pendingProcessJob?.cancel() audioPlayer?.playAudio(R.raw.start) - } override fun onSessionStopped(sessionid:String) { - sendEvent("recognition_stopped", mapOf("sessionid" to sessionid, "status" to "stopped")) - _isRecognitionActive.set(false) + Log.d(TAG, "liwei-------- onSessionStopped: $sessionid") + if (currentRecognitionMode == "push_to_talk") { //按住说话 + if (recognitionResult.isNotEmpty()){ + sendEvent( + "recognition_result", mapOf( + "sessionid" to currsessionAsrId, + "text" to recognitionResult + ) + ) + processWithChatApi(currsessionAsrId, recognitionResult) + } + sendEvent("recognition_stopped", mapOf("sessionid" to currsessionAsrId, "status" to "stopped")) + }else{ + sendEvent("recognition_stopped", mapOf("sessionid" to sessionid, "status" to "stopped")) + + audioPlayer?.playAudio(R.raw.stop) + } stopIdleCheck() - audioPlayer?.playAudio(R.raw.stop) + _isRecognitionActive.set(false) + } override fun onCanceled(sessionid: String,reason: String, errorDetails: String) { + Log.d(TAG, "liwei-------- onCanceled: $sessionid $reason $errorDetails") _isRecognitionActive.set(false) stopIdleCheck() - Log.d(TAG, "recognition_canceled:") BleService.closeCodec() sendEvent( "recognition_canceled", mapOf( @@ -739,6 +812,7 @@ object AgentService : CoroutineScope { } override fun onError(sessionid:String, code: Int, error: String) { + Log.d(TAG, "liwei-------- onError: $sessionid $code $error") _isRecognitionActive.set(false) stopIdleCheck() BleService.closeCodec() @@ -928,6 +1002,7 @@ object AgentService : CoroutineScope { hasImage: Boolean = false, imagePath: String? = null ) { + // 如果有正在进行的AI流式输出,先停止它 audioPlayer?.stopAudio() stopAiStream() @@ -1004,9 +1079,9 @@ object AgentService : CoroutineScope { // 兜底处理,直接添加原消息 addToHistoryMessages(userMessage) } -// Log.e("ChatApiService", "liwei---------sendMessageStream messages:${messagesWithSystemPrompt}") chatApiService.sendMessageStream( sessionid, + vipExptime, messagesWithSystemPrompt, object : StreamCallback { @@ -1014,16 +1089,19 @@ object AgentService : CoroutineScope { sessionid: String, prompt_tokens: Int?, completion_tokens: Int?, - total_tokens: Int? + callfunctions: List? ) { try { - sendStatisticsEvent( - mapOf( - "CHAT_PROMPT_TOKENS" to (prompt_tokens ?: 0), - "CHAT_COMPLETION_TOKENS" to (completion_tokens ?: 0), - "CHAT_TOTAL_TOKENS" to (total_tokens ?: 0) - ) - ) + // 显式构造 Map,避免 mapOf 推断为 Map + val actions = mutableMapOf() + actions["AICHAT_PROMPT_TOKENS"] = (prompt_tokens ?: 0) + actions["AICHAT_COMPLETION_TOKENS"] = (completion_tokens ?: 0) + // 累计函数调用次数:按函数名分别累加 value += 1 + callfunctions?.forEach { funcName -> + val key = "AICHAT_FUNCTION_" + funcName + actions[key] = (actions[key] ?: 0) + 1 + } + // sendStatisticsEvent(actions) }catch (e: Exception) { Log.e(TAG, "liwei--------------- AI Call onUsage 异常", e) } @@ -1032,12 +1110,10 @@ object AgentService : CoroutineScope { override fun onToken(sessionid: String, token: String) { try { if (sessionid != currsessionId) { -// Log.d(TAG, "liwei--------------- AI Call onToken 会话过期 $sessionid-------$currsessionId") - return + return } responseBuilder.append(token) -// Log.d(TAG, "liwei--------------- AI Call onToken:$token speakResponse:$speakResponse reply:$reply broadcast:$broadcast") - if (speakResponse && reply && broadcast && token.isNotEmpty()) { + if (speakResponse && reply && broadcast && token.isNotEmpty()) { ttsService?.speakStream(token) } if (reply && token.isNotEmpty()) { @@ -1106,6 +1182,10 @@ object AgentService : CoroutineScope { "message" to message ) ) + if(code == 2001){ + ttsService?.speakStream(insufficientIntegralText); + } + //育婴提示 // 标记AI流式输出已完成 _isAiStreaming.set(false) currentAiJob = null @@ -1133,9 +1213,9 @@ object AgentService : CoroutineScope { }else if (name == "qqmusic_pause" || name == "music_pause"){ isplaymusic = false } - Log.d(TAG, "liwei--------------- AI Call onFunctionCall iscallingTool:$iscallingTool callingTool:$callingTool") + Log.d(TAG, "liwei--------------- AI Call onFunctionCall iscallingTool:$iscallingTool callingToolText:$callingToolText") if (iscallingTool) { - ttsService?.speakStream(callingTool) + ttsService?.speakStream(callingToolText) iscallingTool = false } } catch (e: Exception) { @@ -1305,26 +1385,18 @@ object AgentService : CoroutineScope { } language = languages[0] return try { - Log.i(TAG, "设置支持的语言: ${languages.joinToString(", ")}") - - - - val asrSuccess = azureAsrHelper?.initialize( + Log.i(TAG, "设置支持的语言: ${languages.joinToString(", ")}") + val asrSuccess = azureAsrHelper?.initialize( subscriptionKey = this.azureSpeechKey, region = this.azureSpeechRegion, supportedLanguages = languages.toTypedArray(), audioSourceType = audioSourceType ) ?: false - if (asrSuccess) { - - recognizeCallback() - + recognizeCallback() } else { Log.w(TAG, "Azure ASR初始化失败") } - - // 同时更新TTS语言设置 val ttsSuccess = updateTtsLanguage(languages.first()) // 使用第一个语言作为TTS语言 @@ -1357,6 +1429,7 @@ object AgentService : CoroutineScope { } } + /** * 更新TTS语言设置 * @@ -1391,6 +1464,12 @@ object AgentService : CoroutineScope { } } + + fun setvipExptime(vipexptime: Int): Boolean { + vipExptime = vipexptime + return true + } + /** * 根据ASR语言代码获取对应的TTS语音角色名称 * 与LanguageManager中的映射保持一致 @@ -2546,16 +2625,18 @@ object AgentService : CoroutineScope { for ((key, value) in actions) { actionsObject.put(key, value) } - - jsonObject.put("actions", actionsObject) + + jsonObject.put("usagetype", 3) // 3 表示AICHAT + jsonObject.put("usages", actionsObject) val json = jsonObject.toString() + Log.d(TAG, "统计事件发送成功,请求: $json") val mediaType = "application/json".toMediaType() val body = json.toRequestBody(mediaType) // 构建请求 val request = Request.Builder() - .url("$serverurl/api/home/user_pushaction") + .url("$serverurl/api/home/user_usages") .addHeader("Authorization", usertoken) .addHeader("Content-Type", "application/json") .post(body) @@ -2570,8 +2651,42 @@ object AgentService : CoroutineScope { override fun onResponse(call: Call, response: Response) { response.use { + // 仅读取一次响应体,避免重复读取导致异常,同时统一后续处理使用 + val bodyStr = response.body?.string() ?: "" if (response.isSuccessful) { - Log.d(TAG, "统计事件发送成功") + Log.d(TAG, "统计事件发送成功,响应: $bodyStr") + try { + val json = JSONObject(bodyStr) + val code = json.optInt("code", -1) + val msg = json.optString("msg", "") + val dataObj = json.optJSONObject("data") + + // 解析 data.usages 为 Map + val dataMap = mutableMapOf() + val usagesObj = dataObj?.optJSONObject("usages") + if (usagesObj != null) { + val keys = usagesObj.keys() + while (keys.hasNext()) { + val k = keys.next() + dataMap[k] = usagesObj.optInt(k, 0) + } + for ((k, v) in dataMap) { + Log.d(TAG, "统计事件发送成功,响应: $k -> $v") + if(k == "4"){ //AI积分更新 + vipExptime = v + // 广播一个细化事件,便于前端单独处理 AI 积分更新 + sendEvent( + "assistant_usages", + mapOf( + "vipxptime" to vipExptime + ) + ) + } + } + } + } catch (e: Exception) { + Log.w(TAG, "解析统计响应失败: ${e.message}") + } } else { Log.w(TAG, "统计事件发送失败,HTTP状态码: ${response.code}") } @@ -2616,4 +2731,4 @@ object AgentService : CoroutineScope { Log.e(TAG, "保存 _isInterrupt 缓存状态时发生异常: ${e.message}", e) } } -} \ No newline at end of file +} diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 39edf4c8d..d53768ecd 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -78,8 +78,14 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream config["musicServiceType"]?.toString()?.let { intent.putExtra("musicServiceType", it) } config["navigationMode"]?.toString()?.let { intent.putExtra("navigationMode", it) } config["isttsinterrupt"]?.toString()?.let { intent.putExtra("isttsinterrupt", it) } - config["callingTool"]?.toString()?.let { intent.putExtra("callingTool", it) } - + config["callingToolText"]?.toString()?.let { intent.putExtra("callingToolText", it) } + config["insufficientIntegralText"]?.toString()?.let { intent.putExtra("insufficientIntegralText", it) } + config["vipexptime"]?.toString()?.let { + intent.putExtra("vipexptime", it) + //打印日志 + Log.d(TAG, "liwei------ vipexptime 2 $it") + } + // 添加 supportedLanguages 参数处理 config["supportedLanguages"]?.let { languages -> @@ -93,7 +99,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream } } } - + intent.action = "AppStart" // 启动前台服务 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -261,7 +267,20 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("SET_LANGUAGES_ERROR", "设置支持语言失败: ${e.message}", null) } } - + "setVipExptime" -> { + try { + val vipexptime = call.argument("vipexptime") + if (vipexptime == null) { + result.error("INVALID_ARGUMENTS", "vipexptime参数不能为空", null) + return + } + val isSuccess = AgentService.setvipExptime(vipexptime) + result.success(isSuccess) + } catch (e: Exception) { + Log.e(TAG, "更新用户信息失败", e) + result.error("UPDATE_USER_INFO_ERROR", "更新用户信息失败: ${e.message}", null) + } + } "stopAiSteam" -> { try { AgentService.stopAiStream() diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index d1b8a1827..6c992fdd3 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -68,7 +68,10 @@ class BleMediaService : Service(), CoroutineScope { private const val KEY_SUPPORTED_LANGUAGES = "supportedLanguages" private const val KEY_MUSICSERVICE_TYPE = "musicServiceType" private const val APP_NAVIGATION_MODE = "navigationMode" - private const val APP_CALLING_TOOL = "callingTool" + private const val APP_CALLING_TOOL_TEXT = "callingToolText" // 工具调用提示 + private const val APP_INSUFFICIENT_INTEGRAL_TEXT = "insufficientIntegralText" // 积分不足提示 + private const val KEY_VIP_EXPTIME = "vipexptime" // 会员到期时间 + // 用于跟踪服务是否正在运行 private val isRunning = AtomicBoolean(false) @@ -490,6 +493,14 @@ private var companionManager: BleCompanionManager? = null // 初始化语音代理 if (!isChatAgentInitialized) { initializeChatAgent() + }else if (intent?.action == "AppStart"){ + val prefs = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + val incoming = extractConfigFromIntent(intent) + if (incoming.isNotEmpty()) { + saveConfigToPreferences(incoming, prefs) + } + val cfg = getConfigFromPreferences(prefs) + AgentService.applyConfigUpdate(cfg) } // 处理蓝牙扫描结果事件 @@ -501,7 +512,6 @@ private var companionManager: BleCompanionManager? = null } return START_STICKY } - return START_STICKY } @@ -577,9 +587,11 @@ private var companionManager: BleCompanionManager? = null it.getStringExtra(KEY_USER_TOKEN)?.let { value -> config[KEY_USER_TOKEN] = value } it.getStringExtra(KEY_MUSICSERVICE_TYPE)?.let { value -> config[KEY_MUSICSERVICE_TYPE] = value } it.getStringExtra(APP_NAVIGATION_MODE)?.let { value -> config[APP_NAVIGATION_MODE] = value } - it.getStringExtra(APP_CALLING_TOOL)?.let { value -> config[APP_CALLING_TOOL] = value } + it.getStringExtra(APP_CALLING_TOOL_TEXT)?.let { value -> config[APP_CALLING_TOOL_TEXT] = value } + it.getStringExtra(APP_INSUFFICIENT_INTEGRAL_TEXT)?.let { value -> config[APP_INSUFFICIENT_INTEGRAL_TEXT] = value } + it.getStringExtra(KEY_VIP_EXPTIME)?.let { value -> config[KEY_VIP_EXPTIME] = value } it.getStringExtra("isttsinterrupt")?.let { value -> config["isttsinterrupt"] = value } - + Log.d(TAG, "liwei-------vipexptime 3-1 ${ config[KEY_VIP_EXPTIME]}") // 提取 supportedLanguages 参数 it.getStringArrayListExtra(KEY_SUPPORTED_LANGUAGES)?.let { languages -> if (languages.isNotEmpty()) { @@ -640,8 +652,10 @@ private var companionManager: BleCompanionManager? = null config[KEY_USER_TOKEN] = getConfigString(prefs, KEY_USER_TOKEN, "") config[KEY_MUSICSERVICE_TYPE] = getConfigString(prefs, KEY_MUSICSERVICE_TYPE, "") config[APP_NAVIGATION_MODE] = getConfigString(prefs, APP_NAVIGATION_MODE, "") - config[APP_CALLING_TOOL] = getConfigString(prefs, APP_CALLING_TOOL, "") - + config[APP_CALLING_TOOL_TEXT] = getConfigString(prefs, APP_CALLING_TOOL_TEXT, "") + config[APP_INSUFFICIENT_INTEGRAL_TEXT] = getConfigString(prefs, APP_INSUFFICIENT_INTEGRAL_TEXT, "") + config[KEY_VIP_EXPTIME] = getConfigString(prefs, KEY_VIP_EXPTIME, "") + Log.d(TAG, "liwei-------vipexptime 3-0 ${ config[KEY_VIP_EXPTIME]}") // 添加 supportedLanguages 参数处理 val supportedLanguagesStr = getConfigString(prefs, KEY_SUPPORTED_LANGUAGES, "") if (supportedLanguagesStr.isNotEmpty()) { @@ -651,7 +665,6 @@ private var companionManager: BleCompanionManager? = null config[KEY_SUPPORTED_LANGUAGES] = languageList } } - return config } @@ -661,7 +674,13 @@ private var companionManager: BleCompanionManager? = null private fun getConfigString(prefs: SharedPreferences, key: String, defaultValue: String): String { return prefs.getString(key, defaultValue) ?: defaultValue } - + + /** + * 从SharedPreferences获取字符串配置 + */ + private fun getConfigInt(prefs: SharedPreferences, key: String, defaultValue: Int): Int { + return prefs.getInt(key, defaultValue) + } override fun onBind(intent: Intent?): IBinder? = null override fun onDestroy() { diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index d07c9b2b0..436a17151 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -65,8 +65,12 @@ class AgentServiceImpl: NSObject { // 会话ID,用于区分不同聊天上下文 private let agentId = "default_agent" //工具调用提示 - public var callingTool = "" + public var callingToolText = "" // 工具调用提示 + public var insufficientIntegralText = "" // 积分不足提示 + private var navigationMode = "" + //会员过期时间 + public var vipexptime = 0 private var isInitialized: Bool = false private var isRecognizing: Bool = false @@ -94,7 +98,10 @@ class AgentServiceImpl: NSObject { var micCapture: MicrophoneCapture! // 语音识别模式 public var currentRecognitionMode: String = "normal" - + // 识别结果 + private var recognitionResult = "" + private var currsessionAsrId: String = "" + private var pendingProcessWorkItem: DispatchWorkItem? internal var audioPlayer: AudioPlayer? // private var locationInfo:[String: String]? @@ -153,7 +160,7 @@ class AgentServiceImpl: NSObject { private func initSystemPrompt() { systemPrompt = """ - 你是小言,一个亲切而温暖的智能AI语音助手,拥有智能、细致而贴心的服务能力。你的任务不仅是高效处理用户的日常事务,更要陪伴用户,提供情感支持和温暖陪伴。你的能力包括: + 你是Lily,一个亲切而温暖的智能AI语音助手,拥有智能、细致而贴心的服务能力。你的任务不仅是高效处理用户的日常事务,更要陪伴用户,提供情感支持和温暖陪伴。你的能力包括: 1. 日程与任务管理:温柔提醒用户的重要日程、待办事项,让用户感到安心和舒适。 2. 日常信息与建议:亲切、迅速地提供天气、新闻、生活小贴士等实用信息。 3. 个人成长陪伴:鼓励并陪伴用户实现个人目标,提出富有同理心的行动建议。 @@ -178,8 +185,17 @@ class AgentServiceImpl: NSObject { self.navigationMode = navigationMode } - if let callingTool = config["callingTool"] as? String { - self.callingTool = callingTool + if let callingToolText = config["callingToolText"] as? String { + self.callingToolText = callingToolText + } + if let vipexptime = config["vipexptime"] as? Int { + self.vipexptime = vipexptime + } + //打印日志输出vipexptime + os_log("Vip 过期时间 %d", log: logger, type: .info, vipexptime) + + if let insufficientIntegralText = config["insufficientIntegralText"] as? String { + self.insufficientIntegralText = insufficientIntegralText } //加载 isInterrupt 状态 @@ -337,8 +353,6 @@ class AgentServiceImpl: NSObject { sendError("初始化ChatAPI服务失败", code: "CHATAPI_INIT_ERROR") return false } - - return true } @@ -374,41 +388,33 @@ class AgentServiceImpl: NSObject { } private func startIdleCheck(idleSeconds: TimeInterval = 10) { - // 2. 所有对idleTimer的操作都通过串行队列执行,避免竞态条件 timerQueue.async { - // 先停止旧的定时器(在串行队列中执行,确保线程安全) - self.stopIdleCheck() - + if self.idleTimer != nil { + self.idleTimer?.cancel() + self.idleTimer = nil + } guard self.isRecognizing else { return } - os_log("启动空闲检测,超时时间: %.0f秒,模式: %{public}@", log: self.logger, type: .info, idleSeconds, self.currentRecognitionMode) - - // 使用全局队列作为定时器回调队列(避免后台挂起) let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility)) - timer.schedule(deadline: .now() + idleSeconds) timer.setEventHandler { [weak self] in guard let self = self else { return } - - // 回调在全局队列,涉及UI或实例变量操作时切换到主线程 DispatchQueue.main.async { [weak self] in guard let self = self else { return } - - // 检查状态(确保在主线程访问这些属性,避免UI相关竞态) - if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming { - os_log("空闲检测触发自动停止,模式: %{public}@", - log: self.logger, - type: .info, - self.currentRecognitionMode) - - // 关闭编码器(设备侧操作,若BleService线程安全可直接调用,否则需加锁) + os_log("空闲检测触发自动停止,模式: %{public}@", + log: self.logger, + type: .info, + self.currentRecognitionMode) + //打电话模式下面 不能关闭识别器 + if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming{ BleService.shared.closeCodec() - - self.stopRecognition() + if (currentRecognitionMode != "phone_call"){ + self.stopRecognition() + } self.sendEvent(name: "auto_stop", data: [ "reason": "idle_timeout", "seconds": idleSeconds, @@ -417,9 +423,7 @@ class AgentServiceImpl: NSObject { } } } - timer.resume() - // 3. 在串行队列中安全赋值 self.idleTimer = timer } } @@ -487,14 +491,15 @@ class AgentServiceImpl: NSObject { print("ai启动语音=\(audioSourceType)=\(useBle)") guard let success = azureAsrHelper?.startContinuousRecognition( // callback: self, - audioSourceType: audioSourceType + audioSourceType: audioSourceType, + isRemoveFirstPunctuation: false ), success else { // 新增:启动失败时复位"启动中"状态 isStartingRecognition = false sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR") return false } - if(useBle){ + if(useBle){ isKeepResult = true } // 根据模式启动相应的空闲检测(此处 guard isRecognizing,会在 onSessionStarted 中再次启动) @@ -713,7 +718,7 @@ class AgentServiceImpl: NSObject { azureTtsHelper?.startspeak(sessionid: sessionid) os_log("设置回调并调用sendMessageStream", log: logger, type: .info) chatApiService.setStreamCallback(sessionid,callback) - chatApiService.sendMessageStream(sessionid,messages: messages) + chatApiService.sendMessageStream(sessionid,vipexptime: vipexptime,messages: messages) audioPlayer?.playAwaitSound() os_log("sendMessageStream调用完成", log: logger, type: .info) } @@ -1513,7 +1518,13 @@ private func jsonToString(_ json: [String: Any]) -> String? { return false } } - + /* + * 设置vip过期时间 + */ + func setVipExptime(_ vipexptime:Int) -> Bool { + self.vipexptime = vipexptime + return true + } /** * 根据ASR语言代码获取对应的TTS语音角色名称 * 与LanguageManager中的映射保持一致 @@ -1690,14 +1701,16 @@ class ChatApiStreamCallback: StreamCallback { return } + if (responseBuilder.isEmpty) { //关闭声音 + try agentService.audioPlayer?.stopAwaitSound() + } + responseBuilder += token - if speakResponse && reply && broadcast{ try agentService.azureTtsHelper?.speakStream(sessionid:sessionid,token) // 在开始流式TTS时立即停止气泡音 } if (reply && token != "") { - try agentService.audioPlayer?.stopAwaitSound() try agentService.sendEvent(name: "assistant_token", data: ["sessionid":sessionid,"token": token]) } }catch { @@ -1807,8 +1820,8 @@ class ChatApiStreamCallback: StreamCallback { ]) if (iscallingTool) { - os_log("收到函数调用: callingTool:%{public}@", log: agentService.logger, type: .info, agentService.callingTool) - agentService.azureTtsHelper?.speakStream(sessionid: sessionid,agentService.callingTool) + os_log("收到函数调用: callingToolText:%{public}@", log: agentService.logger, type: .info, agentService.callingToolText) + agentService.azureTtsHelper?.speakStream(sessionid: sessionid,agentService.callingToolText) iscallingTool = false } @@ -2159,13 +2172,50 @@ class AudioPlayer { extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { func onResult(sessionid:String ,_ text: String, _ detectedLanguage: String) { if !text.isEmpty && isKeepResult { - var data: [String: Any] = ["sessionid":sessionid,"text": text] - if !detectedLanguage.isEmpty { - data["language"] = detectedLanguage + if currsessionAsrId.isEmpty { + currsessionAsrId = sessionid + } + recognitionResult += text + if (currentRecognitionMode == "push_to_talk") { //按住说话 + var data: [String: Any] = [ + "sessionid": currsessionAsrId, + "text": recognitionResult + ] + if !detectedLanguage.isEmpty { + data["language"] = detectedLanguage + } + sendEvent(name: "recognizing", data: data) + }else{ + pendingProcessWorkItem?.cancel() + + var data: [String: Any] = [ + "sessionid": currsessionAsrId, + "text": recognitionResult + ] + if !detectedLanguage.isEmpty { + data["language"] = detectedLanguage + } + sendEvent(name: "recognizing", data: data) + + let workItem = DispatchWorkItem { [weak self] in + guard let self = self else { return } + if !self.recognitionResult.isEmpty { + var resultData: [String: Any] = [ + "sessionid": self.currsessionAsrId, + "text": self.recognitionResult + ] + if !detectedLanguage.isEmpty { + resultData["language"] = detectedLanguage + } + self.sendEvent(name: "recognition_result", data: resultData) + self.processTextInput(sessionid: self.currsessionAsrId, self.recognitionResult, speakResponse: true) + self.recognitionResult = "" + self.currsessionAsrId = "" + } + } + pendingProcessWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5, execute: workItem) } - sendEvent(name: "recognition_result", data: data) - - processTextInput(sessionid: sessionid,text, speakResponse: true) } let previousHasSpeech = hasSpeechDetected @@ -2177,6 +2227,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { } func onRecognizing(sessionid:String ,_ recognizing: String, _ detectedLanguage: String) { + pendingProcessWorkItem?.cancel() if !recognizing.isEmpty && isKeepResult { let previousHasSpeech = hasSpeechDetected hasSpeechDetected = true @@ -2185,13 +2236,19 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { print("onRecognizing restartIdleCheck") restartIdleCheck() } - - var data: [String: Any] = ["sessionid":sessionid,"text": recognizing] + if currsessionAsrId.isEmpty { + currsessionAsrId = sessionid + } + let text = recognitionResult + recognizing + var data: [String: Any] = [ + "sessionid": currsessionAsrId, + "text": text + ] if !detectedLanguage.isEmpty { data["language"] = detectedLanguage } sendEvent(name: "recognizing", data: data) - + if isSpeaking || isAiStreaming { interruptCurrentResponse() } @@ -2200,6 +2257,9 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { func onSessionStarted(sessionid:String) { sendEvent(name: "recognition_started", data: ["sessionid":sessionid,"status": "started"]) + recognitionResult = "" + currsessionAsrId = "" + pendingProcessWorkItem?.cancel() isRecognizing = true hasSpeechDetected = false // 新增:会话真正启动,复位"启动中"状态 @@ -2220,13 +2280,28 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { } func onSessionStopped(sessionid:String) { - sendEvent(name: "recognition_stopped", data: ["sessionid":sessionid,"status": "stopped"]) - isRecognizing = false - hasSpeechDetected = false - stopIdleCheck() - if (currentRecognitionMode == "ble_wakeup") { - audioPlayer?.playStopSound() + if (currentRecognitionMode == "push_to_talk") { //按住说话 + if (!recognitionResult.isEmpty){ + var data: [String: Any] = [ + "sessionid": currsessionAsrId, + "text": recognitionResult + ] + sendEvent(name: "recognition_result", data: data) + processTextInput(sessionid: currsessionAsrId, recognitionResult, speakResponse: true) + } + sendEvent(name: "recognition_stopped", data: ["sessionid":currsessionAsrId,"status": "stopped"]) + }else{ + sendEvent(name: "recognition_stopped", data: ["sessionid":sessionid,"status": "stopped"]) + isRecognizing = false + hasSpeechDetected = false + stopIdleCheck() + if (currentRecognitionMode == "ble_wakeup") { + audioPlayer?.playStopSound() + } } + + + } func onCanceled(sessionid:String ,_ reason: String, _ errorDetails: String) { diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift index 0877ab63e..cad33bcb4 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift @@ -178,7 +178,14 @@ public class AgentServicePlugin: NSObject, FlutterPlugin { let success = impl.setSupportedLanguages(languages) result(success) - + case "setVipExptime": + guard let arguments = call.arguments as? [String: Any], + let vipexptime = arguments["vipexptime"] as? Int else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "vipexptime参数不能为空", details: nil)) + return + } + let success = impl.setVipExptime(vipexptime) + result(success) default: result(FlutterMethodNotImplemented) } diff --git a/local_plugins/agent_service/lib/agent_service.dart b/local_plugins/agent_service/lib/agent_service.dart index 689a17c6c..6d1017ae9 100644 --- a/local_plugins/agent_service/lib/agent_service.dart +++ b/local_plugins/agent_service/lib/agent_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:ffi'; import 'package:flutter/services.dart'; @@ -59,6 +60,9 @@ enum AgentServiceEventType { /// 函数调用结果 functionCallResult, + /// AI助手使用统计 + assistantUsages, + /// 图片处理中 imageProcessing, @@ -167,6 +171,8 @@ class AgentService { return AgentServiceEventType.functionCall; case 'function_call_result': return AgentServiceEventType.functionCallResult; + case 'assistant_usages': + return AgentServiceEventType.assistantUsages; case 'image_processing': return AgentServiceEventType.imageProcessing; case 'image_ready': @@ -484,6 +490,29 @@ class AgentService { } } + /// 更新用户信息 + /// + /// 更新用户信息,包括姓名、手机号、邮箱等 + /// [config] 配置参数,应包含以下参数: + /// - azureSpeechKey: Azure语音服务密钥 + /// - azureSpeechRegion: Azure语音服务区域 + /// - openaiApiKey: OpenAI API密钥 + /// - openaiBaseUrl: (可选) OpenAI API 基础URL + /// - openaiModel: (可选) OpenAI模型名称,默认为"gpt-3.5-turbo" + /// - mcpServer: (可选) MCP服务器地址 + /// + /// 返回是否成功启动服务 + static Future setVipExptime(int vipexptime) async { + try { + final bool result = await _channel.invokeMethod('setVipExptime', { + 'vipexptime': vipexptime, + }); + return result; + } on PlatformException catch (e) { + throw AgentServiceException(e.code, e.message ?? '更新用户信息失败', e.details); + } + } + /// 释放资源 static Future dispose() async { try { diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 2f0989c62..343c5ca00 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -24,795 +24,882 @@ import java.util.UUID */ class AzureAsrHelper(private val context: Context) { - private val tag = "AzureAsrHelper" + private val tag = "AzureAsrHelper" - // 核心组件 - private var speechConfig: SpeechConfig? = null - private var recognizer: SpeechRecognizer? = null - private var audioConfig: AudioConfig? = null - private var continuousCallback: ContinuousRecognizeCallback? = null + // 核心组件 + private var speechConfig: SpeechConfig? = null + private var recognizer: SpeechRecognizer? = null + private var audioConfig: AudioConfig? = null + private var continuousCallback: ContinuousRecognizeCallback? = null - // 添加前台服务管理标志 - private var isForegroundServiceRunning = false - // 配置参数 - private var currentLanguage = "zh-CN" - private var supportedLanguages = arrayOf("zh-CN") - private var isAutoDetectLanguage = false - private var subscriptionKey = "" - private var region = "" + // 添加前台服务管理标志 + private var isForegroundServiceRunning = false - // 音频源配置 - var audioSourceType = AudioSourceType.MICROPHONE + // 配置参数 + private var currentLanguage = "zh-CN" + private var supportedLanguages = arrayOf("zh-CN") + private var isAutoDetectLanguage = false + private var subscriptionKey = "" + private var region = "" - // 音频处理 - Initialize immediately to avoid null checks - var audioStream: SimpleAudioReceiver = SimpleAudioReceiver(context) + private var useXunfei = false + private var xunFeiAsrHelper: XunFeiAsrHelper? = null + + // 音频源配置 + var audioSourceType = AudioSourceType.MICROPHONE + + // 音频处理 - Initialize immediately to avoid null checks + var audioStream: SimpleAudioReceiver = SimpleAudioReceiver(context) // 网络状态监听 - private var networkMonitor: NetworkStateMonitor = NetworkStateMonitor(context) - private var isNetworkRecovering = false - + private var networkMonitor: NetworkStateMonitor = NetworkStateMonitor(context) + private var isNetworkRecovering = false + // 网络丢失计数器 - private var networkLostCount = 0 + private var networkLostCount = 0 + // 最大网络丢失次数(3秒检测) - private val maxNetworkLostCount = 3 - + private val maxNetworkLostCount = 3 + // 防抖机制相关变量 - private var lastNetworkLostTime = 0L - private val networkLostDebounceInterval = 3000L // 2秒防抖间隔 - private var currsessionid = "" //识别回话id 关联到整个聊天过程中 - + private var lastNetworkLostTime = 0L + private val networkLostDebounceInterval = 3000L // 2秒防抖间隔 + private var currsessionid = "" //识别回话id 关联到整个聊天过程中 - /** - * 音频来源类型 - */ - enum class AudioSourceType { - /** 使用设备麦克风 */ - MICROPHONE, - /** 使用外部提供的音频数据 */ - EXTERNAL - } + /** + * 音频来源类型 + */ + enum class AudioSourceType { + /** 使用设备麦克风 */ + MICROPHONE, + /** 使用外部提供的音频数据 */ + EXTERNAL + } - /** - * 初始化Azure语音服务 - * - * @param subscriptionKey Azure 订阅密钥 - * @param region Azure 区域 - * @param supportedLanguages 支持的语言数组 - * @param audioSourceType 音频源类型 - * @return 初始化是否成功 - */ - fun initialize( - subscriptionKey: String, - region: String, - supportedLanguages: Array = arrayOf("zh-CN", "en-US"), - audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE - ): Boolean { - try { - // 检查配置是否为空 - if (subscriptionKey.isEmpty() || region.isEmpty()) { - Log.e(tag, "Azure 配置信息不完整") - return false - } - // 释放之前的资源 - dispose() + /** + * 初始化Azure语音服务 + * + * @param subscriptionKey Azure 订阅密钥 + * @param region Azure 区域 + * @param supportedLanguages 支持的语言数组 + * @param audioSourceType 音频源类型 + * @return 初始化是否成功 + */ + fun initialize( + subscriptionKey: String, + region: String, + supportedLanguages: Array = arrayOf("zh-CN", "en-US"), + audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE + ): Boolean { + try { + // 检查配置是否为空 + if (subscriptionKey.isEmpty() || region.isEmpty()) { + Log.e(tag, "Azure 配置信息不完整") + return false + } + + // 释放之前的资源 + dispose() + + // 保存配置 + this.subscriptionKey = subscriptionKey + this.region = region + this.audioSourceType = audioSourceType + + // 设置语言 + if (supportedLanguages.isNotEmpty()) { + this.supportedLanguages = supportedLanguages + } + + this.useXunfei = supportedLanguages.all { it == "zh-CN" || it == "en-US" } + + // 根据支持的语言数量决定是否启用自动语言检测 + this.isAutoDetectLanguage = supportedLanguages.size >= 2 + + // 如果只有一种语言,设置为当前语言 + if (!isAutoDetectLanguage && supportedLanguages.isNotEmpty()) { + this.currentLanguage = supportedLanguages[0] + } + + // 创建语音配置 + speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region).apply { + if (isAutoDetectLanguage) { + // 直接启用语言检测模式 + setProperty(PropertyId.SpeechServiceConnection_LanguageIdMode, "Continuous") + } else { + // 设置指定的识别语言 + speechRecognitionLanguage = currentLanguage + } - // 保存配置 - this.subscriptionKey = subscriptionKey - this.region = region - this.audioSourceType = audioSourceType + setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", "300") + setProperty("Speech_SegmentationSilenceTimeoutMs", "300") + + // 优化:添加低延迟连接配置 + setProperty("SpeechServiceConnection_InitialSilenceTimeoutMs", "200") + setProperty("SpeechServiceConnection_RecoMode", "INTERACTIVE") + setProperty("Speech_PushStreamFormat", "PCM") + + // 设置分段策略为时间模式 + setProperty("Speech_SegmentationStrategy", "Time") + } + // 初始化网络监听 + networkMonitor.initialize(object : NetworkStateMonitor.NetworkStateListener { + override fun onNetworkAvailable() { + handleNetworkAvailable() + } - // 设置语言 - if (supportedLanguages.isNotEmpty()) { - this.supportedLanguages = supportedLanguages - } + override fun onNetworkLost() { + handleNetworkLost() + } + }) - // 根据支持的语言数量决定是否启用自动语言检测 - this.isAutoDetectLanguage = supportedLanguages.size >= 2 + return true + } catch (e: Exception) { + Log.e(tag, "初始化失败: ${e.message}") + return false + } + } + + // /** + // * 设置音频配置 + // * @param sampleRate 采样率 (8000, 16000, 24000, 32000, 44100, 48000) + // * @param channels 声道数 (1=单声道, 2=立体声) + // */ + // fun setAudioConfig(sampleRate: Int, channels: Int) { + // try { + // // 设置RecordFile的音频配置 + // recordfile?.setAudioConfig(sampleRate, channels) + // Log.d(tag, "音频配置已设置: 采样率=${sampleRate}Hz, 声道数=${channels}") + // } catch (e: Exception) { + // Log.e(tag, "设置音频配置失败: ${e.message}") + // } + // } + + private fun isRecognizerValid(): Boolean { + return recognizer != null + } - // 如果只有一种语言,设置为当前语言 - if (!isAutoDetectLanguage && supportedLanguages.isNotEmpty()) { - this.currentLanguage = supportedLanguages[0] - } + /** + * 设置识别器 + */ + private fun setupRecognizer(): Boolean { + try { + // 如果正在进行连续识别,先停止 + if (audioStream.isContinuousRecognitionActive) { + // 直接停止,不等待结果 + recognizer?.stopContinuousRecognitionAsync() + audioStream.isContinuousRecognitionActive = false + } + setupMicrophoneStream() + + recognizer?.close() + recognizer = null + //创建识别器 + recognizer = if (isAutoDetectLanguage) { + val autoDetectConfig = + AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) + SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig) + } else { + SpeechRecognizer(speechConfig, audioConfig) + } + + return true + } catch (e: Exception) { + Log.e(tag, "创建识别器失败: ${e.message}") + stopAudioProcessing() + return false + } + } - // 创建语音配置 - speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region).apply { - if (isAutoDetectLanguage) { - // 直接启用语言检测模式 - setProperty(PropertyId.SpeechServiceConnection_LanguageIdMode, "Continuous") - } else { - // 设置指定的识别语言 - speechRecognitionLanguage = currentLanguage - } - - setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", "300") - setProperty("Speech_SegmentationSilenceTimeoutMs", "300") - - // 优化:添加低延迟连接配置 - setProperty("SpeechServiceConnection_InitialSilenceTimeoutMs", "200") - setProperty("SpeechServiceConnection_RecoMode", "INTERACTIVE") - setProperty("Speech_PushStreamFormat", "PCM") - - // 设置分段策略为时间模式 - setProperty("Speech_SegmentationStrategy", "Time") - } - // 初始化网络监听 - networkMonitor.initialize(object : NetworkStateMonitor.NetworkStateListener { - override fun onNetworkAvailable() { - handleNetworkAvailable() - } - - override fun onNetworkLost() { - handleNetworkLost() - } - }) - - return true - } catch (e: Exception) { - Log.e(tag, "初始化失败: ${e.message}") - return false - } + /** + * 设置麦克风流 - 使用拉流方式 + */ + private fun setupMicrophoneStream() { + try { + Log.d(tag, "设置麦克风流 - 使用拉流方式: }") + + // Initialize if not already done + if (!audioStream.isInitialized()) { + audioStream.initAudioRecord() + } + audioStream.setupStream() + audioConfig = AudioConfig.fromStreamInput(audioStream.pushAudioStream) + + } catch (e: Exception) { + Log.e(tag, "设置麦克风流失败: ${e.message}") } + } + - // /** - // * 设置音频配置 - // * @param sampleRate 采样率 (8000, 16000, 24000, 32000, 44100, 48000) - // * @param channels 声道数 (1=单声道, 2=立体声) - // */ - // fun setAudioConfig(sampleRate: Int, channels: Int) { - // try { - // // 设置RecordFile的音频配置 - // recordfile?.setAudioConfig(sampleRate, channels) - // Log.d(tag, "音频配置已设置: 采样率=${sampleRate}Hz, 声道数=${channels}") - // } catch (e: Exception) { - // Log.e(tag, "设置音频配置失败: ${e.message}") - // } - // } - - private fun isRecognizerValid(): Boolean { - return recognizer != null + /** + * 开始连续语音识别 + * @param audioSourceType 音频源类型 + * @param audioDataCallback 音频数据回调 + * @param isRemoveFirstPunctuation 是否移除第一个标点符号 + * @return 是否成功开始识别 + */ + fun startContinuousRecognition( + audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE, + audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null, + isRemoveFirstPunctuation: Boolean = true + ): Boolean { + + Log.d(tag, "startContinuousRecognition:${audioStream.isContinuousRecognitionActive} ") + + // 检查网络状态 + if (!checkNetworkStatus()) { + Log.e(tag, "网络不可用,无法开始语音识别") + continuousCallback?.onError(currsessionid, 1000, "网络连接不可用,请检查网络设置") + return false } - /** - * 设置识别器 - */ - private fun setupRecognizer(): Boolean { - try { - // 如果正在进行连续识别,先停止 - if (audioStream.isContinuousRecognitionActive) { - // 直接停止,不等待结果 - recognizer?.stopContinuousRecognitionAsync() - audioStream.isContinuousRecognitionActive = false - } - setupMicrophoneStream() - - recognizer?.close() - recognizer = null - //创建识别器 - recognizer = if (isAutoDetectLanguage) { - val autoDetectConfig = - AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) - SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig) - } else { - SpeechRecognizer(speechConfig, audioConfig) - } + if (audioStream.isContinuousRecognitionActive) { + return true + } - return true - } catch (e: Exception) { - Log.e(tag, "创建识别器失败: ${e.message}") - stopAudioProcessing() - return false - } + // 启动前台服务(在开始音频录制前) + if (!isForegroundServiceRunning && audioSourceType == AudioSourceType.MICROPHONE) { + try { + AudioRecordingForegroundService.startService(context) + isForegroundServiceRunning = true + Log.d(tag, "前台服务已启动") + } catch (e: Exception) { + Log.e(tag, "启动前台服务失败: ${e.message}") + continuousCallback?.onError(currsessionid, 1003, "启动前台服务失败: ${e.message}") + return false + } } - /** - * 设置麦克风流 - 使用拉流方式 - */ - private fun setupMicrophoneStream() { - try { - Log.d(tag, "设置麦克风流 - 使用拉流方式: }") + if (useXunfei) { + Log.d(tag, "Using Xunfei Recognition") + val callback = continuousCallback + if (callback == null) { + Log.e(tag, "无法启动连续识别:回调为空") + continuousCallback?.onError(currsessionid, 1001, "回调为空") + return false + } - // Initialize if not already done - if (!audioStream.isInitialized()) { - audioStream.initAudioRecord() - } - audioStream.setupStream() - audioConfig = AudioConfig.fromStreamInput(audioStream.pushAudioStream) + if (xunFeiAsrHelper == null) { + xunFeiAsrHelper = XunFeiAsrHelper(context) + } - } catch (e: Exception) { - Log.e(tag, "设置麦克风流失败: ${e.message}") + try { + xunFeiAsrHelper?.start(callback, currentLanguage, isAutoDetectLanguage, isRemoveFirstPunctuation) + + this.audioSourceType = audioSourceType + if (!audioStream.isContinuousRecognitionActive) { + // 确保音频流已初始化 + if (!audioStream.isInitialized()) { + audioStream.initAudioRecord() + audioStream.setupStream() + } + + audioStream.startAudioRecord( + when (audioSourceType) { + AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE + AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL + }, + object : SimpleAudioReceiver.AudioDataCallback { + override fun onAudio(data: ByteArray) { + audioDataCallback?.onAudio(data) + xunFeiAsrHelper?.sendAudio(data) + } + } + ) } + audioStream.isContinuousRecognitionActive = true + return true + } catch (e: Exception) { + Log.e(tag, "Start Xunfei failed: ${e.message}") + xunFeiAsrHelper?.stop() + audioStream.isContinuousRecognitionActive = false + return false + } } + if (!isRecognizerValid()) { + Log.d(tag, "重新启动连续识别") + val callback = continuousCallback + if (callback == null) { + Log.e(tag, "无法重新启动连续识别:回调为空") + continuousCallback?.onError(currsessionid, 1001, "回调为空") + return false + } + setupEventListeners(callback) + } + this.audioSourceType = audioSourceType + + try { + Log.d(tag, "startContinuousRecognition: ${audioStream.isWriting()}") + // 启动音频处理(若已启动则跳过) + if (!audioStream.isContinuousRecognitionActive) { + Log.d(tag, "startAudioRecord:$audioSourceType") + audioStream.startAudioRecord( + when (audioSourceType) { + AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE + AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL + }, + audioDataCallback + ) + } else { + Log.d(tag, "音频已启动,跳过重复启动") + } + + recognizer?.startContinuousRecognitionAsync() + audioStream.isContinuousRecognitionActive = true + return true + } catch (e: Exception) { + Log.e(tag, "开始连续识别失败: ${e.message}") + stopContinuousRecognition() + audioStream.isContinuousRecognitionActive = false + return false + } + } - /** - * 开始连续语音识别 - * @param audioSourceType 音频源类型 - * @param audioDataCallback 音频数据回调 - * @return 是否成功开始识别 - */ - fun startContinuousRecognition( - audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE, - audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null - ): Boolean { - - Log.d(tag, "startContinuousRecognition:${audioStream.isContinuousRecognitionActive} ") - - // 检查网络状态 - if (!checkNetworkStatus()) { - Log.e(tag, "网络不可用,无法开始语音识别") - continuousCallback?.onError(currsessionid,1000,"网络连接不可用,请检查网络设置") - return false - } - - if (audioStream.isContinuousRecognitionActive) { - return true - } - - // 启动前台服务(在开始音频录制前) - if (!isForegroundServiceRunning && audioSourceType == AudioSourceType.MICROPHONE) { - try { - AudioRecordingForegroundService.startService(context) - isForegroundServiceRunning = true - Log.d(tag, "前台服务已启动") - } catch (e: Exception) { - Log.e(tag, "启动前台服务失败: ${e.message}") - continuousCallback?.onError(currsessionid,1003, "启动前台服务失败: ${e.message}") - return false - } - } - - if (!isRecognizerValid()) { - Log.d(tag, "重新启动连续识别") - val callback = continuousCallback - if (callback == null) { - Log.e(tag, "无法重新启动连续识别:回调为空") - continuousCallback?.onError(currsessionid,1001,"回调为空") - return false + /** + * 设置事件监听器 + */ + fun setupEventListeners(callback: ContinuousRecognizeCallback): Boolean { + Log.d(tag, "设置ssssss监听器:${speechConfig ?: "null"} ") + if (speechConfig == null) { + callback.onError(currsessionid, 1002, "语音服务未初始化") + return false + } + + // 重设识别器 + if (!setupRecognizer()) { + Log.d(tag, "初始化失败: ") + return false + } + try { + continuousCallback = callback; + Log.d(tag, "设置ssssss监听器: ") + // 优化:识别中事件 - 添加文本长度检查 + recognizer?.recognizing?.addEventListener( + EventHandler { _, event -> + // 优化:只处理非空结果 + if (event.result.text.isNotEmpty()) { + val detectedLanguage = if (isAutoDetectLanguage) { + AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: "" + } else { + currentLanguage } - setupEventListeners(callback) + callback.onRecognizing(currsessionid, event.result.text, detectedLanguage) + } } - this.audioSourceType = audioSourceType - - try { - Log.d(tag, "startContinuousRecognition: ${audioStream.isWriting()}") - // 启动音频处理(若已启动则跳过) - if (!audioStream.isContinuousRecognitionActive) { - Log.d(tag, "startAudioRecord:$audioSourceType") - audioStream.startAudioRecord( - when (audioSourceType) { - AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE - AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL - }, - audioDataCallback - ) + ) + + // 优化:识别完成事件 - 优化语言检测 + recognizer?.recognized?.addEventListener( + EventHandler { _, event -> + if (event.result.reason == ResultReason.RecognizedSpeech && event.result.text.isNotEmpty()) { + val detectedLanguage = if (isAutoDetectLanguage) { + AutoDetectSourceLanguageResult.fromResult(event.result)?.language + ?: supportedLanguages[0] } else { - Log.d(tag, "音频已启动,跳过重复启动") + currentLanguage } - - recognizer?.startContinuousRecognitionAsync() - audioStream.isContinuousRecognitionActive = true - return true - } catch (e: Exception) { - Log.e(tag, "开始连续识别失败: ${e.message}") - stopContinuousRecognition() - audioStream.isContinuousRecognitionActive = false - return false + callback.onResult(currsessionid, event.result.text, detectedLanguage) + currsessionid = UUID.randomUUID().toString() + } } - } + ) + + // 会话开始事件 + recognizer?.sessionStarted?.addEventListener( + EventHandler { _, _ -> + // 直接在当前线程调用回调 + Log.d(tag, "会话开始事件") + currsessionid = UUID.randomUUID().toString() + callback.onSessionStarted(currsessionid) + } + ) + + // 会话结束事件 + recognizer?.sessionStopped?.addEventListener( + EventHandler { _, _ -> + // 直接在当前线程调用回调 + Log.d(tag, "会话结束事件") + // if (audioSourceType == AudioSourceType.EXTERNAL) { + callback.onSessionStopped(currsessionid) + audioStream?.isContinuousRecognitionActive = false + // } + } + ) - /** - * 设置事件监听器 - */ - fun setupEventListeners(callback: ContinuousRecognizeCallback): Boolean { - Log.d(tag, "设置ssssss监听器:${speechConfig ?: "null"} ") - if (speechConfig == null) { - callback.onError(currsessionid,1002,"语音服务未初始化") - return false + // 取消事件 + recognizer?.canceled?.addEventListener( + EventHandler { _, event -> + val errorDetails = event.errorDetails ?: "未知错误" + val reason = event.reason.toString() + + Log.d(tag, "识别被取消: reason=$reason, details=$errorDetails") + callback.onCanceled(currsessionid, reason, errorDetails) } + ) + } catch (e: Exception) { + callback.onError(currsessionid, 1001, "启动连续识别失败: ${e.message}") + return false + } + return true + } - // 重设识别器 - if (!setupRecognizer()) { - Log.d(tag, "初始化失败: ") - return false + + /** + * 停止连续语音识别 + * + * @return 是否成功停止 + */ + fun stopContinuousRecognition(): Boolean { + if (useXunfei) { + try { + xunFeiAsrHelper?.stop() + stopAudioProcessing() + audioStream.isContinuousRecognitionActive = false + + if (isForegroundServiceRunning) { + try { + AudioRecordingForegroundService.stopService(context) + isForegroundServiceRunning = false + } catch (e: Exception) { + Log.e(tag, "停止前台服务失败: ${e.message}") + } } + return true + } catch (e: Exception) { + Log.e(tag, "Stop Xunfei failed: ${e.message}") + return false + } + } + + if (speechConfig == null) { + Log.e(tag, "语音服务未初始化") + return false + } + try { + Log.d(tag, "停止连续语音识别: ") + // 停止音频处理 + audioStream.stopMicrophoneCapture() + // 直接停止连续识别(SDK内部已是异步操作) + recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS) + + recognizer?.close() + recognizer = null + audioStream.isContinuousRecognitionActive = false + + // 停止前台服务 + if (isForegroundServiceRunning) { try { - continuousCallback = callback; - Log.d(tag, "设置ssssss监听器: ") - // 优化:识别中事件 - 添加文本长度检查 - recognizer?.recognizing?.addEventListener( - EventHandler { _, event -> - // 优化:只处理非空结果 - if (event.result.text.isNotEmpty()) { - val detectedLanguage = if (isAutoDetectLanguage) { - AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: "" - } else { - currentLanguage - } - callback.onRecognizing(currsessionid,event.result.text, detectedLanguage) - } - } - ) - - // 优化:识别完成事件 - 优化语言检测 - recognizer?.recognized?.addEventListener( - EventHandler { _, event -> - if (event.result.reason == ResultReason.RecognizedSpeech && event.result.text.isNotEmpty()) { - val detectedLanguage = if (isAutoDetectLanguage) { - AutoDetectSourceLanguageResult.fromResult(event.result)?.language - ?: supportedLanguages[0] - } else { - currentLanguage - } - callback.onResult(currsessionid,event.result.text, detectedLanguage) - currsessionid = UUID.randomUUID().toString() - } - } - ) - - // 会话开始事件 - recognizer?.sessionStarted?.addEventListener( - EventHandler { _, _ -> - // 直接在当前线程调用回调 - Log.d(tag, "会话开始事件") - currsessionid = UUID.randomUUID().toString() - callback.onSessionStarted(currsessionid) - } - ) - - // 会话结束事件 - recognizer?.sessionStopped?.addEventListener( - EventHandler { _, _ -> - // 直接在当前线程调用回调 - Log.d(tag, "会话结束事件") - if (audioSourceType == AudioSourceType.EXTERNAL) { - callback.onSessionStopped(currsessionid) - audioStream?.isContinuousRecognitionActive = false - } - } - ) - - // 取消事件 - recognizer?.canceled?.addEventListener( - EventHandler { _, event -> - val errorDetails = event.errorDetails ?: "未知错误" - val reason = event.reason.toString() - - Log.d(tag, "识别被取消: reason=$reason, details=$errorDetails") - callback.onCanceled(currsessionid,reason,errorDetails) - } - ) + AudioRecordingForegroundService.stopService(context) + isForegroundServiceRunning = false + Log.d(tag, "前台服务已停止") } catch (e: Exception) { - callback.onError(currsessionid,1001,"启动连续识别失败: ${e.message}") - return false + Log.e(tag, "停止前台服务失败: ${e.message}") } - return true + } + + return true + } catch (e: Exception) { + // 强制重置状态 + audioStream.isContinuousRecognitionActive = false + Log.e(tag, "停止连续识别失败: ${e.message}") + // 停止音频处理 + audioStream.stopMicrophoneCapture() + recognizer?.stopContinuousRecognitionAsync() + recognizer?.close() + recognizer = null + audioStream.isContinuousRecognitionActive = false + + // 确保前台服务被停止 + if (isForegroundServiceRunning) { + try { + AudioRecordingForegroundService.stopService(context) + isForegroundServiceRunning = false + } catch (e2: Exception) { + Log.e(tag, "强制停止前台服务失败: ${e2.message}") + } + } + + return false } + } + /** + * 检查连续识别是否活跃 + */ + fun isContinuousRecognitionActive(): Boolean = audioStream.isContinuousRecognitionActive - /** - * 停止连续语音识别 - * - * @return 是否成功停止 - */ - fun stopContinuousRecognition(): Boolean { - if (speechConfig == null) { - Log.e(tag, "语音服务未初始化") - return false + /** + * 释放所有资源 + */ + fun dispose() { + try { + // 如果正在进行连续识别,先停止 + if (audioStream.isContinuousRecognitionActive) { + if (useXunfei) { + xunFeiAsrHelper?.stop() + } else { + // 直接停止,不等待结果 + recognizer?.stopContinuousRecognitionAsync() } + audioStream.isContinuousRecognitionActive = false + } + // 停止音频处理 + stopAudioProcessing() + // 清理网络监听 + clearNetworkDetection() + // 停止录音 + audioStream.recordfile?.closeFile(true) + + // 释放recognizer + recognizer?.close() + recognizer = null + + xunFeiAsrHelper?.stop() + xunFeiAsrHelper = null + + // 释放speechConfig + speechConfig?.close() + speechConfig = null + + // 释放音频配置 + audioConfig?.close() + audioConfig = null + + // 确保前台服务被停止 + if (isForegroundServiceRunning) { try { - Log.d(tag, "停止连续语音识别: ") - // 停止音频处理 - audioStream.stopMicrophoneCapture() - // 直接停止连续识别(SDK内部已是异步操作) - recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS) - - recognizer?.close() - recognizer = null - audioStream.isContinuousRecognitionActive = false - - // 停止前台服务 - if (isForegroundServiceRunning) { - try { - AudioRecordingForegroundService.stopService(context) - isForegroundServiceRunning = false - Log.d(tag, "前台服务已停止") - } catch (e: Exception) { - Log.e(tag, "停止前台服务失败: ${e.message}") - } - } - - return true + AudioRecordingForegroundService.stopService(context) + isForegroundServiceRunning = false + Log.d(tag, "dispose时前台服务已停止") } catch (e: Exception) { - // 强制重置状态 - audioStream.isContinuousRecognitionActive = false - Log.e(tag, "停止连续识别失败: ${e.message}") - // 停止音频处理 - audioStream.stopMicrophoneCapture() - recognizer?.stopContinuousRecognitionAsync() - recognizer?.close() - recognizer = null - audioStream.isContinuousRecognitionActive = false - - // 确保前台服务被停止 - if (isForegroundServiceRunning) { - try { - AudioRecordingForegroundService.stopService(context) - isForegroundServiceRunning = false - } catch (e2: Exception) { - Log.e(tag, "强制停止前台服务失败: ${e2.message}") - } - } - - return false + Log.e(tag, "dispose时停止前台服务失败: ${e.message}") } + } + + // 确保状态被重置 + audioStream.isContinuousRecognitionActive = false + currsessionid = "" + } catch (e: Exception) { + // 确保状态被重置 + audioStream.isContinuousRecognitionActive = false + audioConfig = null + recognizer = null + speechConfig = null + isForegroundServiceRunning = false + currsessionid = "" } + } - /** - * 检查连续识别是否活跃 - */ - fun isContinuousRecognitionActive(): Boolean = audioStream.isContinuousRecognitionActive + // 音频处理相关方法 + /** + * 停止音频处理 + */ + private fun stopAudioProcessing() { + try { + Log.e(tag, "停止音频处理: }") + audioStream.releaseAudioResources() + audioStream.pushAudioStream?.close() + } catch (e: Exception) { + Log.e(tag, "关闭麦克风流失败: ${e.message}") + e.printStackTrace() + } + } - /** - * 释放所有资源 - */ - fun dispose() { - try { - // 如果正在进行连续识别,先停止 - if (audioStream.isContinuousRecognitionActive) { - // 直接停止,不等待结果 - recognizer?.stopContinuousRecognitionAsync() - audioStream.isContinuousRecognitionActive = false - } - // 停止音频处理 - stopAudioProcessing() - // 清理网络监听 - clearNetworkDetection() - // 停止录音 - audioStream.recordfile?.closeFile(true) - - // 释放recognizer - recognizer?.close() - recognizer = null - - // 释放speechConfig - speechConfig?.close() - speechConfig = null - - // 释放音频配置 - audioConfig?.close() - audioConfig = null - - // 确保前台服务被停止 - if (isForegroundServiceRunning) { - try { - AudioRecordingForegroundService.stopService(context) - isForegroundServiceRunning = false - Log.d(tag, "dispose时前台服务已停止") - } catch (e: Exception) { - Log.e(tag, "dispose时停止前台服务失败: ${e.message}") - } - } - - // 确保状态被重置 - audioStream.isContinuousRecognitionActive = false - currsessionid = "" - } catch (e: Exception) { - // 确保状态被重置 - audioStream.isContinuousRecognitionActive = false - audioConfig = null - recognizer = null - speechConfig = null - isForegroundServiceRunning = false - currsessionid = "" - } + +//录音音频文件 + /** + * 开启录音 + */ + fun enableRecord( + audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE, + filePath: String, + audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null + ) { + Log.i(tag, "开启录音:") + this.audioSourceType = audioSourceType + if (audioSourceType == AudioSourceType.EXTERNAL) { + Log.i(tag, "外部音频源不在这里录音") + return } - // 音频处理相关方法 - /** - * 停止音频处理 - */ - private fun stopAudioProcessing() { + // 启动前台服务(在开始录音前) + if (!isForegroundServiceRunning) { + try { + AudioRecordingForegroundService.startService(context) + isForegroundServiceRunning = true + Log.d(tag, "录音前台服务已启动") + } catch (e: Exception) { + Log.e(tag, "启动录音前台服务失败: ${e.message}") + } + } + if (audioStream.recordfile == null) { + audioStream.recordfile = RecordFile() + } + + if (!audioStream.isContinuousRecognitionActive) { + // 启动音频处理 + Log.i(tag, "开启录音:audioSourceType${audioSourceType}") + // Initialize if not already done + if (!audioStream.isInitialized()) { + audioStream.initAudioRecord() + } + audioStream.setupStream() + + + audioStream.startAudioRecord( + when (audioSourceType) { + AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE + AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL + }, + audioDataCallback + ) + } + + audioStream.isRecord = true + audioStream.recordfile?.closeFile(true) + audioStream.recordfile?.creatingFiles(filePath) + } + + /** + * 移动文件到新路径 + */ + fun moveFile(sourcePath: String, destPath: String): Boolean { + if (audioSourceType == AudioSourceType.EXTERNAL) { + Log.i(tag, "外部音频源不在这里移动文件") + return false + } + Log.i(tag, "移动文件到新路径:") + audioStream.recordfile?.moveFile(sourcePath, destPath) ?: return false + return true + } + + /** + * 重命名指定路径的音频文件 + */ + fun renameFile(filePath: String, newName: String): Boolean { + if (audioSourceType == AudioSourceType.EXTERNAL) { + Log.i(tag, "外部音频源不在这里重命名文件") + return false + } + Log.i(tag, "重命名指定路径的音频文件:") + audioStream.recordfile?.renameFile(filePath, newName) ?: return false + return true + } + + /** + * 停止录音 + */ + fun pauseRecord() { + if (audioSourceType == AudioSourceType.EXTERNAL) { + Log.i(tag, "外部音频源不在这里暂停录音") + return + } + Log.i(tag, "停止连续录音:") + audioStream.recordfile ?: return + audioStream.isRecord = false + } + + /** + * 继续录音 + */ + fun resumeRecord() { + if (audioSourceType == AudioSourceType.EXTERNAL) { + Log.i(tag, "外部音频源不在这里继续录音") + return + } + Log.i(tag, "继续录音:") + audioStream.recordfile ?: return + audioStream.isRecord = true + } + + /** + * 关闭录音 + */ + fun stopRecord(isSave: Boolean) { + if (audioSourceType == AudioSourceType.EXTERNAL) { + Log.i(tag, "外部音频源不在这里关闭录音") + return + } + Log.i(tag, "关闭录音:") + audioStream.recordfile ?: return + + if (!audioStream.isContinuousRecognitionActive) { + audioStream.stopMicrophoneCapture() + + // 停止录音时也停止前台服务(如果没有其他音频任务) + if (isForegroundServiceRunning) { try { - Log.e(tag, "停止音频处理: }") - audioStream.releaseAudioResources() - audioStream.pushAudioStream?.close() + AudioRecordingForegroundService.stopService(context) + isForegroundServiceRunning = false + Log.d(tag, "录音前台服务已停止") } catch (e: Exception) { - Log.e(tag, "关闭麦克风流失败: ${e.message}") - e.printStackTrace() + Log.e(tag, "停止录音前台服务失败: ${e.message}") } + } } - - + audioStream.isRecord = false + audioStream.recordfile?.closeFile(isSave) + } -//录音音频文件 + /** + * 一次性识别回调接口 + */ + interface RecognizeCallback { /** - * 开启录音 + * 返回识别结果 + * + * @param text 识别的文本 + * @param detectedLanguage 检测到的语言 */ - fun enableRecord(audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE,filePath: String, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = null) { - Log.i(tag, "开启录音:") - this.audioSourceType = audioSourceType - if(audioSourceType == AudioSourceType.EXTERNAL){ - Log.i(tag, "外部音频源不在这里录音") - return - } - - // 启动前台服务(在开始录音前) - if (!isForegroundServiceRunning) { - try { - AudioRecordingForegroundService.startService(context) - isForegroundServiceRunning = true - Log.d(tag, "录音前台服务已启动") - } catch (e: Exception) { - Log.e(tag, "启动录音前台服务失败: ${e.message}") - } - } - if (audioStream.recordfile == null) { - audioStream.recordfile = RecordFile() - } - - if (!audioStream.isContinuousRecognitionActive) { - // 启动音频处理 - Log.i(tag, "开启录音:audioSourceType${audioSourceType}") - // Initialize if not already done - if (!audioStream.isInitialized()) { - audioStream.initAudioRecord() - } - audioStream.setupStream() - - - audioStream.startAudioRecord( when (audioSourceType) { - AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE - AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL - }, - audioDataCallback - ) - } - - audioStream.isRecord = true - audioStream.recordfile?.closeFile(true) - audioStream.recordfile?.creatingFiles(filePath) - } + fun onResult(text: String, detectedLanguage: String) /** - * 移动文件到新路径 + * 识别错误时调用 + * + * @param code 错误码 + * @param error 错误信息 */ - fun moveFile(sourcePath: String, destPath: String): Boolean { - if(audioSourceType == AudioSourceType.EXTERNAL){ - Log.i(tag, "外部音频源不在这里移动文件") - return false - } - Log.i(tag, "移动文件到新路径:") - audioStream.recordfile?.moveFile(sourcePath, destPath) ?: return false - return true - } + fun onError(code: Int, error: String) + } + /** + * 连续识别回调接口 + */ + interface ContinuousRecognizeCallback { /** - * 重命名指定路径的音频文件 + * 返回识别结果 + * + * @param text 识别的文本 + * @param detectedLanguage 检测到的语言 */ - fun renameFile(filePath: String, newName: String): Boolean { - if(audioSourceType == AudioSourceType.EXTERNAL){ - Log.i(tag, "外部音频源不在这里重命名文件") - return false - } - Log.i(tag, "重命名指定路径的音频文件:") - audioStream.recordfile?.renameFile(filePath, newName) ?: return false - return true - } + fun onResult(sessiond: String, text: String, detectedLanguage: String) /** - * 停止录音 + * 识别进行中调用 + * + * @param recognizing 正在识别的文本 + * @param detectedLanguage 检测到的语言 */ - fun pauseRecord() { - if(audioSourceType == AudioSourceType.EXTERNAL){ - Log.i(tag, "外部音频源不在这里暂停录音") - return - } - Log.i(tag, "停止连续录音:") - audioStream.recordfile ?: return - audioStream.isRecord = false - } + fun onRecognizing(sessiond: String, recognizing: String, detectedLanguage: String) /** - * 继续录音 + * 会话开始时调用 */ - fun resumeRecord() { - if(audioSourceType == AudioSourceType.EXTERNAL){ - Log.i(tag, "外部音频源不在这里继续录音") - return - } - Log.i(tag, "继续录音:") - audioStream.recordfile ?: return - audioStream.isRecord = true - } + fun onSessionStarted(sessiond: String) /** - * 关闭录音 + * 会话结束时调用 */ - fun stopRecord(isSave: Boolean) { - if(audioSourceType == AudioSourceType.EXTERNAL){ - Log.i(tag, "外部音频源不在这里关闭录音") - return - } - Log.i(tag, "关闭录音:") - audioStream.recordfile ?: return - - if (!audioStream.isContinuousRecognitionActive) { - audioStream.stopMicrophoneCapture() - - // 停止录音时也停止前台服务(如果没有其他音频任务) - if (isForegroundServiceRunning) { - try { - AudioRecordingForegroundService.stopService(context) - isForegroundServiceRunning = false - Log.d(tag, "录音前台服务已停止") - } catch (e: Exception) { - Log.e(tag, "停止录音前台服务失败: ${e.message}") - } - } - } - - audioStream.isRecord = false - audioStream.recordfile?.closeFile(isSave) - } - + fun onSessionStopped(sessiond: String) /** - * 一次性识别回调接口 + * 识别取消时调用 + * + * @param reason 取消原因 + * @param errorDetails 错误详情 */ - interface RecognizeCallback { - /** - * 返回识别结果 - * - * @param text 识别的文本 - * @param detectedLanguage 检测到的语言 - */ - fun onResult(text: String, detectedLanguage: String) - - /** - * 识别错误时调用 - * - * @param code 错误码 - * @param error 错误信息 - */ - fun onError(code: Int,error: String) - } + fun onCanceled(sessiond: String, reason: String, errorDetails: String) /** - * 连续识别回调接口 + * 识别出错时调用 + * + * @param error 错误信息 */ - interface ContinuousRecognizeCallback { - /** - * 返回识别结果 - * - * @param text 识别的文本 - * @param detectedLanguage 检测到的语言 - */ - fun onResult(sessiond:String, text: String, detectedLanguage: String) - - /** - * 识别进行中调用 - * - * @param recognizing 正在识别的文本 - * @param detectedLanguage 检测到的语言 - */ - fun onRecognizing(sessiond:String, recognizing: String, detectedLanguage: String) - - /** - * 会话开始时调用 - */ - fun onSessionStarted(sessiond:String) - - /** - * 会话结束时调用 - */ - fun onSessionStopped(sessiond:String) - - - /** - * 识别取消时调用 - * - * @param reason 取消原因 - * @param errorDetails 错误详情 - */ - fun onCanceled(sessiond:String,reason: String, errorDetails: String) - - /** - * 识别出错时调用 - * - * @param error 错误信息 - */ - fun onError(sessiond:String, code: Int,error: String) - } + fun onError(sessiond: String, code: Int, error: String) + } - /** - * 检查当前网络状态 - * @return true表示网络可用,false表示网络不可用 - */ - private fun checkNetworkStatus(): Boolean { - return networkMonitor.checkNetworkStatus() - } /** - * 检查是否是网络相关错误 - */ - private fun isNetworkRelatedError(reason: String, errorDetails: String): Boolean { - return networkMonitor.isNetworkRelatedError(reason, errorDetails) + * 检查当前网络状态 + * @return true表示网络可用,false表示网络不可用 + */ + private fun checkNetworkStatus(): Boolean { + return networkMonitor.checkNetworkStatus() + } + + /** + * 检查是否是网络相关错误 + */ + private fun isNetworkRelatedError(reason: String, errorDetails: String): Boolean { + return networkMonitor.isNetworkRelatedError(reason, errorDetails) + } + + /** + * 处理网络连接可用 + */ + private fun handleNetworkAvailable() { + // 网络恢复时重置计数器 + networkLostCount = 0 + lastNetworkLostTime = 0L + if (!isNetworkRecovering) { + isNetworkRecovering = true + Log.d(tag, "网络恢复,准备重新启动识别") } - - /** - * 处理网络连接可用 - */ - private fun handleNetworkAvailable() { - // 网络恢复时重置计数器 - networkLostCount = 0 - lastNetworkLostTime = 0L - if (!isNetworkRecovering) { - isNetworkRecovering = true - Log.d(tag, "网络恢复,准备重新启动识别") - } + } + + /** + * 处理网络连接丢失(带防抖机制) + */ + private fun handleNetworkLost() { + val currentTime = System.currentTimeMillis() + + // 防抖机制:如果距离上次触发时间小于防抖间隔,则忽略本次触发 + if (currentTime - lastNetworkLostTime < networkLostDebounceInterval) { + Log.d(tag, "网络丢失事件被防抖过滤,距离上次触发仅${currentTime - lastNetworkLostTime}ms") + return } - - /** - * 处理网络连接丢失(带防抖机制) - */ - private fun handleNetworkLost() { - val currentTime = System.currentTimeMillis() - - // 防抖机制:如果距离上次触发时间小于防抖间隔,则忽略本次触发 - if (currentTime - lastNetworkLostTime < networkLostDebounceInterval) { - Log.d(tag, "网络丢失事件被防抖过滤,距离上次触发仅${currentTime - lastNetworkLostTime}ms") - return - } - - // 更新最后触发时间 - lastNetworkLostTime = currentTime + + // 更新最后触发时间 + lastNetworkLostTime = currentTime - //这里启动一个3秒定时器,3秒后检查网络是否恢复 - // 启动3秒定时器,3秒后检查网络是否恢复 + //这里启动一个3秒定时器,3秒后检查网络是否恢复 + // 启动3秒定时器,3秒后检查网络是否恢复 Handler(Looper.getMainLooper()).postDelayed({ - Log.e(tag, "网络不可用") - val isNetworkAvailable = checkNetworkStatus() - if (isNetworkAvailable) { - // 如果网络恢复,重置计数器和防抖时间 - Log.d(tag, "网络已恢复,重置计数器") - lastNetworkLostTime = 0L - isNetworkRecovering = true - } else { - continuousCallback?.onError(currsessionid,1000,"网络连接不可用,请检查网络设置") - Log.d(tag,"网络仍未恢复,继续检测") - networkLostCount = 0 + Log.e(tag, "网络不可用") + val isNetworkAvailable = checkNetworkStatus() + if (isNetworkAvailable) { + // 如果网络恢复,重置计数器和防抖时间 + Log.d(tag, "网络已恢复,重置计数器") + lastNetworkLostTime = 0L + isNetworkRecovering = true + } else { + continuousCallback?.onError(currsessionid, 1000, "网络连接不可用,请检查网络设置") + Log.d(tag, "网络仍未恢复,继续检测") + networkLostCount = 0 stopAudioProcessingImmediately() - } - }, 3000L) // 3秒延时 - } - - /** - * 立即停止音频处理(网络断开时使用) - */ - private fun stopAudioProcessingImmediately() { - try { - Log.d(tag, "立即停止音频处理") - stopContinuousRecognition() - // 不改变isContinuousRecognitionActive状态,保持识别意图 - } catch (e: Exception) { - Log.e(tag, "立即停止音频处理失败: ${e.message}") - } + } + }, 3000L) // 3秒延时 + } + + /** + * 立即停止音频处理(网络断开时使用) + */ + private fun stopAudioProcessingImmediately() { + try { + Log.d(tag, "立即停止音频处理") + stopContinuousRecognition() + // 不改变isContinuousRecognitionActive状态,保持识别意图 + } catch (e: Exception) { + Log.e(tag, "立即停止音频处理失败: ${e.message}") } -/** - * 清理网络检测相关资源 - */ -private fun clearNetworkDetection() { + } + + /** + * 清理网络检测相关资源 + */ + private fun clearNetworkDetection() { networkLostCount = 0 lastNetworkLostTime = 0L -} + } } diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index 9436c1841..eccd2e060 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt @@ -449,6 +449,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { "startContinuousRecognition" -> { val useExternalAudio = call.argument("audioSourceType") ?: false + val removeFirstPunctuation = call.argument("isRemoveFirstPunctuation") ?: true // 确保事件通道已准备好 if (asrEventSink == null) { result.error( @@ -468,7 +469,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { AzureAsrHelper.AudioSourceType.MICROPHONE } - val success = azureAsrHelper.startContinuousRecognition(audioSourceType) + val success = azureAsrHelper.startContinuousRecognition(audioSourceType, isRemoveFirstPunctuation = removeFirstPunctuation) result.success(true) } catch (e: Exception) { result.error("START_RECOGNITION_ERROR", e.message, null) @@ -698,6 +699,11 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { result.success(azureTtsHelper.setVoice(voiceName)) } + "setVoiceFlocking" -> { + val speakerProfileId = call.argument("speakerProfileId") ?: "" + result.success(azureTtsHelper.setVoiceFlocking(speakerProfileId)) + } + "speakText" -> { val text = call.argument("text") ?: return result.error( "INVALID_ARGUMENTS", diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt index 261825b98..4b47782aa 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt @@ -51,6 +51,9 @@ class AzureTtsHelper(private val context: Context) : ITtsService { private val streamBuffer = StringBuilder() private var lastSpeakTime = 0L + private var enableVoiceFlocking = false + private var speakerProfileId: String = "" + /** * 初始化TTS引擎 * @@ -354,6 +357,19 @@ class AzureTtsHelper(private val context: Context) : ITtsService { } } + /** + * 开启声音复刻 + */ + fun setVoiceFlocking(id: String): Boolean { + if(id.isNotEmpty()) { + enableVoiceFlocking = true + } else { + enableVoiceFlocking = false + } + speakerProfileId = id + return true + } + /** * 重新创建合成器 */ @@ -502,16 +518,27 @@ class AzureTtsHelper(private val context: Context) : ITtsService { .replace(Regex("[😀-🟿]+"), "") // 简化表情符号移除 .trim() - // 优化:简化SSML结构,减少嵌套层级 - return """ - - - - $processedText - - - - """.trimIndent() + if(enableVoiceFlocking) { + return """ + + + + $processedText + + + + """.trimIndent() + } else { + return """ + + + + $processedText + + + + """.trimIndent() + } } /** diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/XunFeiAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/XunFeiAsrHelper.kt new file mode 100644 index 000000000..9975fda83 --- /dev/null +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/XunFeiAsrHelper.kt @@ -0,0 +1,274 @@ +package com.yunqiinnovation.azure_speech + +import android.content.Context +import android.util.Base64 +import android.util.Log +import okhttp3.* +import okio.ByteString.Companion.toByteString +import org.json.JSONObject +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.util.UUID +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +class XunFeiAsrHelper(private val context: Context) { + private val tag = "XunFeiAsrHelper" + + private val appId = "43d87d22" + private val accessKeyId = "2bd01ce06ac35957fc0c082f9b6284ec" + private val accessKeySecret = "ODU3OTUyYjhjMjY5NDhkMmMxYWY2Zjlm" + + private var webSocket: WebSocket? = null + private val client = OkHttpClient() + + private var currentSessionId = "" + private var sessionId = "" + private var lastIntermediateResult = "" + + private var currentLanguage = "zh-CN" + private var isAutoDetectLanguage = false + + // 是否第去除一个文字的标点符号 + private var removeFirstPunctuation = true + + fun start(callback: AzureAsrHelper.ContinuousRecognizeCallback, language: String, isAutoDetect: Boolean, isRemoveFirstPunctuation: Boolean) { + try { + webSocket?.cancel() + webSocket = null + } catch (e: Exception) { + Log.e(tag, "Clear previous websocket failed: ${e.message}") + } + + currentSessionId = UUID.randomUUID().toString() + lastIntermediateResult = "" + + currentLanguage = language + isAutoDetectLanguage = isAutoDetect + removeFirstPunctuation = isRemoveFirstPunctuation + + val url = getWebSocketUrl() + val request = Request.Builder().url(url).build() + + webSocket = client.newWebSocket(request, object : WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) { + Log.d(tag, "WebSocket Opened") + callback.onSessionStarted(currentSessionId) + } + + override fun onMessage(webSocket: WebSocket, text: String) { + renderResult(text, callback) + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + Log.d(tag, "WebSocket Closing: $code / $reason") + webSocket.close(1000, null) + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + Log.d(tag, "WebSocket Closed: $code / $reason") + if (currentSessionId.isNotEmpty() && lastIntermediateResult.isNotEmpty()) { + val detectedLanguage = if (isAutoDetectLanguage) { + if (lastIntermediateResult.contains(Regex("[\\u4e00-\\u9fa5]"))) { + "zh-CN" + } else { + "en-US" + } + } else { + currentLanguage + } + callback.onResult(currentSessionId, lastIntermediateResult, detectedLanguage) + } + callback.onSessionStopped(currentSessionId) + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + Log.e(tag, "WebSocket Error", t) + callback.onError(currentSessionId, 1004, "Xunfei connection failed: ${t.message}") + } + }) + } + + fun sendAudio(frameBuffer: ByteArray) { + webSocket?.let { ws: WebSocket -> + ws.send(frameBuffer.toByteString(0, frameBuffer.size)) + } + } + + fun stop() { + try { + webSocket?.let { ws: WebSocket -> + ws.send("{\"end\": true, \"sessionId\": \"$sessionId\"}") + ws.close(1000, "User stopped") + } + } catch (e: Exception) { + Log.e(tag, "Stop Xunfei failed: ${e.message}") + } finally { + webSocket = null + } + } + + private fun renderResult( + resultData: String, + callback: AzureAsrHelper.ContinuousRecognizeCallback + ) { + try { + val jsonData = JSONObject(resultData) + Log.d(tag, "-------------------$jsonData") + val msgType = jsonData.optString("msg_type") + + when (msgType) { + "action" -> { + val sid = jsonData.optString("sessionId") + if (!sid.isNullOrEmpty()) { + sessionId = sid + } + Log.d(tag, "Handshake success") + } + + "result" -> { + val dataStr = jsonData.optString("data") + val data = JSONObject(dataStr) + + val ls = data.optBoolean("ls") + val cn = data.optJSONObject("cn") ?: return + val st = cn.optJSONObject("st") ?: return + val rt = st.optJSONArray("rt") ?: return + + var resultTextTemp = "" + var punctuationCount = 0 + var wordCount = 0 + + for (i in 0 until rt.length()) { + val j = rt.getJSONObject(i) + val ws = j.optJSONArray("ws") ?: continue + for (k in 0 until ws.length()) { + val kObj = ws.getJSONObject(k) + val cw = kObj.optJSONArray("cw") ?: continue + if(removeFirstPunctuation) { + // 如果resultTextTemp中第一个文字是标点符号,则去除第一个文字的标点符号 + for (l in 0 until cw.length()) { + val lObj = cw.getJSONObject(l) + val w = lObj.optString("w") + val wp = lObj.optString("wp") + + if (wp == "p") { + punctuationCount++ + if (resultTextTemp.isEmpty()) { + continue + } + } else { + wordCount++ + } + resultTextTemp += w + } + } else { + // 不去除resultTextTemp第一个文字的标点符号 + for (l in 0 until cw.length()) { + val lObj = cw.getJSONObject(l) + resultTextTemp += lObj.optString("w") + if (lObj.optString("wp") == "p") { + punctuationCount++ + } else { + wordCount++ + } + } + } + } + } + + if (ls && wordCount == 0 && punctuationCount == 1) { + return + } + + val detectedLanguage = if (isAutoDetectLanguage) { + if (resultTextTemp.contains(Regex("[\\u4e00-\\u9fa5]"))) { + "zh-CN" + } else { + "en-US" + } + } else { + currentLanguage + } + + if (st.optInt("type") == 0) { + callback.onResult(currentSessionId, resultTextTemp, detectedLanguage) + currentSessionId = UUID.randomUUID().toString() + lastIntermediateResult = "" + } else { + lastIntermediateResult = resultTextTemp + callback.onRecognizing(currentSessionId, resultTextTemp, detectedLanguage) + } + } + + "error" -> { + callback.onError(currentSessionId, 1005, "Xunfei error: $resultData") + Log.e(tag, "Error: $resultData") + } + + else -> { + callback.onError(currentSessionId, 1006, "Xunfei unknown msg_type: $msgType") + Log.w(tag, "Unknown msg_type: $msgType") + } + } + } catch (e: Exception) { + Log.e(tag, "Parse error", e) + } + } + + private fun getWebSocketUrl(): String { + val baseWsUrl = "wss://office-api-ast-dx.iflyaisol.com/ast/communicate/v1" + + val params = java.util.TreeMap() + params["audio_encode"] = "pcm_s16le" + params["lang"] = "autodialect" + params["samplerate"] = "16000" + params["accessKeyId"] = accessKeyId + params["appId"] = appId + params["utc"] = getUtcTime() + + val signature = calculateSignature(params) + params["signature"] = signature + + val sb = StringBuilder() + var first = true + for ((key, value) in params) { + if (!first) { + sb.append("&") + } + sb.append(URLEncoder.encode(key, "UTF-8")).append("=").append(URLEncoder.encode(value, "UTF-8")) + first = false + } + + return "$baseWsUrl?$sb" + } + + private fun getUtcTime(): String { + val sdf = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + sdf.timeZone = java.util.TimeZone.getTimeZone("GMT+8") + return sdf.format(java.util.Date()) + } + + private fun calculateSignature(params: Map): String { + val baseStr = StringBuilder() + var first = true + for ((key, value) in params) { + if ("signature" == key) continue + if (value.isEmpty()) continue + + if (!first) { + baseStr.append("&") + } + baseStr.append(URLEncoder.encode(key, "UTF-8")).append("=").append(URLEncoder.encode(value, "UTF-8")) + first = false + } + + val mac = Mac.getInstance("HmacSHA1") + val keySpec = SecretKeySpec(accessKeySecret.toByteArray(StandardCharsets.UTF_8), "HmacSHA1") + mac.init(keySpec) + val signBytes = mac.doFinal(baseStr.toString().toByteArray(StandardCharsets.UTF_8)) + + return Base64.encodeToString(signBytes, Base64.NO_WRAP) + } +} diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index 8fe11e856..f6e66af2a 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -57,6 +57,9 @@ public class AzureAsrHelper: NSObject { private var subscriptionKey = "" private var region = "" + private var useXunfei = false + private var xunFeiAsrHelper: XunFeiAsrHelper? + // 音频源配置 public enum AudioSourceType { /** 使用设备麦克风 */ @@ -112,6 +115,9 @@ public class AzureAsrHelper: NSObject { self.supportedLanguages = supportedLanguages } + // 检查是否只包含中文或英文,如果是则使用讯飞 + self.useXunfei = supportedLanguages.allSatisfy { $0 == "zh-CN" || $0 == "en-US" } + // 根据支持的语言数量决定是否启用自动语言检测 self.isAutoDetectLanguage = supportedLanguages.count >= 2 @@ -382,6 +388,7 @@ public class AzureAsrHelper: NSObject { * * @param audioSourceType 音频源类型 * @param audioDataCallback 音频数据回调 + * @param isRemoveFirstPunctuation 是否移除第一个标点符号 * @return 是否成功开始识别(调度成功即返回 true) * * 修复点:在音频启动成功后的闭包中,先判断 audioStream?.isContinuousRecognitionActive 是否仍然为 true, @@ -389,7 +396,8 @@ public class AzureAsrHelper: NSObject { */ public func startContinuousRecognition( audioSourceType: AudioSourceType = .microphone, - audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil + audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil, + isRemoveFirstPunctuation: Bool = true ) -> Bool { guard speechConfig != nil else { print("语音服务未初始化") @@ -413,6 +421,43 @@ public class AzureAsrHelper: NSObject { self.audioSourceType = audioSourceType print("startContinuousRecognition:准备启动") + // 如果使用讯飞识别 + if useXunfei { + print("Using Xunfei Recognition") + guard let callback = self.continuousCallback else { + print("无法启动连续识别:回调为空") + return false + } + + if xunFeiAsrHelper == nil { + xunFeiAsrHelper = XunFeiAsrHelper() + } + + // 启动讯飞识别 + xunFeiAsrHelper?.start(callback: callback, language: currentLanguage, isAutoDetect: isAutoDetectLanguage, isRemoveFirstPunctuation: isRemoveFirstPunctuation) + + // 创建代理回调,将音频数据转发给讯飞 + let proxyCallback = AudioDataCallbackProxy(original: audioDataCallback) { [weak self] data in + self?.xunFeiAsrHelper?.sendAudio(frameBuffer: data) + } + + // 异步启动音频处理 + startAudioRecordAsync(audioSourceType: audioSourceType, audioDataCallback: proxyCallback) { [weak self] (success: Bool) in + guard let self = self else { return } + + if success { + self.audioStream?.isContinuousRecognitionActive = true + os_log("讯飞连续识别已启动", log: self.log, type: .info) + } else { + os_log("音频启动失败,无法开始讯飞识别", log: self.log, type: .error) + self.xunFeiAsrHelper?.stop() + self.audioStream?.isContinuousRecognitionActive = false + } + } + + return true + } + do { if recognizer == nil { // 检查continuousCallback是否为空,只有在不为空时才设置事件监听器 @@ -464,6 +509,23 @@ public class AzureAsrHelper: NSObject { * - 返回: 停止操作是否成功 */ public func stopContinuousRecognition() -> Bool { + if useXunfei { + xunFeiAsrHelper?.stop() + + // 关键:取消尚未执行的启动任务,防止 stop 后音频又被启动 + cancelPendingAudioStart() + + stopAudioRecordAsync { [weak self] (success: Bool) in + guard let self = self else { return } + if success { + self.audioStream?.isContinuousRecognitionActive = false + os_log("讯飞音频停止成功", log: self.log, type: .info) + } + } + self.audioStream?.isContinuousRecognitionActive = false + return true + } + guard speechConfig != nil else { os_log("语音服务未初始化", log: log, type: .error) return false @@ -545,8 +607,12 @@ public class AzureAsrHelper: NSObject { print("释放所有资源:") // 如果正在进行连续识别,先停止 if audioStream?.isContinuousRecognitionActive == true { - // 直接停止,不等待结果 - try? recognizer?.stopContinuousRecognition() + if useXunfei { + xunFeiAsrHelper?.stop() + } else { + // 直接停止,不等待结果 + try? recognizer?.stopContinuousRecognition() + } audioStream?.isContinuousRecognitionActive = false } // 取消防抖任务 - 只取消开始操作的防抖 @@ -560,6 +626,8 @@ public class AzureAsrHelper: NSObject { speechConfig = nil audioConfig = nil audioStream = nil + xunFeiAsrHelper?.stop() + xunFeiAsrHelper = nil // 确保状态被重置 audioStream?.isContinuousRecognitionActive = false //externalAudioStream = nil @@ -739,6 +807,9 @@ public class AzureAsrHelper: NSObject { self.supportedLanguages = [validatedSourceLang, validatedTargetLang] self.currentLanguage = validatedSourceLang + // 更新是否使用讯飞 + self.useXunfei = supportedLanguages.allSatisfy { $0 == "zh-CN" || $0 == "en-US" } + // 重新配置语言检测 self.isAutoDetectLanguage = (validatedSourceLang != validatedTargetLang) @@ -1084,7 +1155,7 @@ extension AzureAsrHelper: NetworkStateMonitor.NetworkStateListener { self.handleNetworkLost() } - // .MARK: - 启动任务管理 + // MARK: - 启动任务管理 /// 取消尚未执行的音频启动任务,防止 stop 后仍然触发启动 private func cancelPendingAudioStart() { audioStartWorkItem?.cancel() @@ -1093,6 +1164,22 @@ extension AzureAsrHelper: NetworkStateMonitor.NetworkStateListener { } +// MARK: - AudioDataCallback Proxy +private class AudioDataCallbackProxy: SimpleAudioReceiver.AudioDataCallback { + private let originalCallback: SimpleAudioReceiver.AudioDataCallback? + private let xunfeiCallback: ((Data) -> Void)? + + init(original: SimpleAudioReceiver.AudioDataCallback?, xunfei: ((Data) -> Void)?) { + self.originalCallback = original + self.xunfeiCallback = xunfei + } + + func onAudio(_ data: Data) { + originalCallback?.onAudio(data) + xunfeiCallback?(data) + } +} + diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift index e55663cc4..194208bd0 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift @@ -371,6 +371,8 @@ private func sendAstEvent(_ event: [String: Any]) { return } + let removeFirstPunctuation = args["isRemoveFirstPunctuation"] as? Bool ?? true + // 根据参数确定音频源类型 let audioSourceType = isExternalActive ? AzureAsrHelper.AudioSourceType.external : @@ -380,7 +382,8 @@ private func sendAstEvent(_ event: [String: Any]) { // 启动连续识别 let success = azureAsrHelper.startContinuousRecognition( - audioSourceType: audioSourceType + audioSourceType: audioSourceType, + isRemoveFirstPunctuation: removeFirstPunctuation ) result(success) case "recognizeCallback": diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/XunFeiAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/XunFeiAsrHelper.swift new file mode 100644 index 000000000..9f8dbdde2 --- /dev/null +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/XunFeiAsrHelper.swift @@ -0,0 +1,328 @@ +import Foundation +import CryptoKit + +class XunFeiAsrHelper: NSObject, URLSessionWebSocketDelegate { + private let tag = "XunFeiAsrHelper" + + private let appId = "43d87d22" + private let accessKeyId = "2bd01ce06ac35957fc0c082f9b6284ec" + private let accessKeySecret = "ODU3OTUyYjhjMjY5NDhkMmMxYWY2Zjlm" + + private var webSocketTask: URLSessionWebSocketTask? + private lazy var session: URLSession = { + let configuration = URLSessionConfiguration.default + return URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue()) + }() + + private var currentSessionId = "" + private var sessionId = "" + private var lastIntermediateResult = "" + + private var currentLanguage = "zh-CN" + private var isAutoDetectLanguage = false + + // 是否第去除一个文字的标点符号 + private var removeFirstPunctuation = true + + private var callback: AzureAsrHelper.ContinuousRecognizeCallback? + + func start(callback: AzureAsrHelper.ContinuousRecognizeCallback, language: String, isAutoDetect: Bool, isRemoveFirstPunctuation: Bool) { + stop() + + self.callback = callback + self.currentLanguage = language + self.isAutoDetectLanguage = isAutoDetect + self.removeFirstPunctuation = isRemoveFirstPunctuation + self.currentSessionId = UUID().uuidString + self.lastIntermediateResult = "" + self.sessionId = "" + + let urlString = getWebSocketUrl() + guard let url = URL(string: urlString) else { + print("\(tag): Invalid URL") + return + } + + let request = URLRequest(url: url) + webSocketTask = session.webSocketTask(with: request) + webSocketTask?.resume() + + print("\(tag): WebSocket Connecting...") + receiveMessage() + } + + func sendAudio(frameBuffer: Data) { + guard let webSocketTask = webSocketTask else { return } + let message = URLSessionWebSocketTask.Message.data(frameBuffer) + webSocketTask.send(message) { error in + if let error = error { + print("\(self.tag): Send audio failed: \(error)") + } + } + } + + func stop() { + if let webSocketTask = webSocketTask { + let endJson = "{\"end\": true, \"sessionId\": \"\(sessionId)\"}" + let message = URLSessionWebSocketTask.Message.string(endJson) + webSocketTask.send(message) { _ in } + webSocketTask.cancel(with: .normalClosure, reason: "User stopped".data(using: .utf8)) + } + webSocketTask = nil + } + + private func receiveMessage() { + webSocketTask?.receive { [weak self] result in + guard let self = self else { return } + + switch result { + case .failure(let error): + print("\(self.tag): WebSocket Receive Error: \(error)") + self.callback?.onError(sessionid: self.currentSessionId, 1004, "Xunfei connection failed: \(error.localizedDescription)") + + case .success(let message): + switch message { + case .string(let text): + self.processMessage(text) + case .data(let data): + if let text = String(data: data, encoding: .utf8) { + self.processMessage(text) + } + @unknown default: + break + } + + // Continue receiving messages + self.receiveMessage() + } + } + } + + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { + print("\(tag): WebSocket Opened") + callback?.onSessionStarted(sessionid: currentSessionId) + } + + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + let reasonString = reason.flatMap { String(data: $0, encoding: .utf8) } ?? "Unknown" + print("\(tag): WebSocket Closed: \(closeCode) / \(reasonString)") + + if !currentSessionId.isEmpty && !lastIntermediateResult.isEmpty { + let detectedLanguage = detectLanguage(from: lastIntermediateResult) + callback?.onResult(sessionid: currentSessionId, lastIntermediateResult, detectedLanguage) + } + callback?.onSessionStopped(sessionid: currentSessionId) + } + + private func processMessage(_ text: String) { + guard let data = text.data(using: .utf8) else { return } + + do { + guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { return } + + let msgType = json["msg_type"] as? String + + switch msgType { + case "action": + if let sid = json["sessionId"] as? String, !sid.isEmpty { + self.sessionId = sid + } + print("\(tag): Handshake success") + + case "result": + processResult(json) + + case "error": + let errorMsg = "\(json)" + print("\(tag): Error: \(errorMsg)") + callback?.onError(sessionid: currentSessionId, 1005, "Xunfei error: \(errorMsg)") + + default: + print("\(tag): Unknown msg_type: \(msgType ?? "nil")") + callback?.onError(sessionid: currentSessionId, 1006, "Xunfei unknown msg_type: \(msgType ?? "nil")") + } + + } catch { + print("\(tag): Parse error: \(error)") + } + } + + private func processResult(_ json: [String: Any]) { + var data: [String: Any]? = nil + + if let dataDict = json["data"] as? [String: Any] { + data = dataDict + } else if let dataStr = json["data"] as? String, + let dataData = dataStr.data(using: .utf8) { + data = try? JSONSerialization.jsonObject(with: dataData, options: []) as? [String: Any] + } + + guard let data = data else { + print("\(tag): Failed to parse data field from result") + return + } + + var ls = false + if let lsBool = data["ls"] as? Bool { + ls = lsBool + } else if let lsInt = data["ls"] as? Int { + ls = (lsInt != 0) + } else if let lsStr = data["ls"] as? String { + ls = (lsStr == "true" || lsStr == "1") + } + + guard let cn = data["cn"] as? [String: Any], + let st = cn["st"] as? [String: Any], + let rt = st["rt"] as? [[String: Any]] else { + return + } + + var resultTextTemp = "" + var punctuationCount = 0 + var wordCount = 0 + + for j in rt { + guard let ws = j["ws"] as? [[String: Any]] else { continue } + for kObj in ws { + guard let cw = kObj["cw"] as? [[String: Any]] else { continue } + + if(removeFirstPunctuation) { + // 如果resultTextTemp中第一个文字是标点符号,则去除第一个文字的标点符号 + for lObj in cw { + let w = lObj["w"] as? String ?? "" + let wp = lObj["wp"] as? String ?? "" + + if wp == "p" { + punctuationCount += 1 + if resultTextTemp.isEmpty { + continue + } + } else { + wordCount += 1 + } + resultTextTemp += w + } + } else { + // 不去除resultTextTemp第一个文字的标点符号 + for lObj in cw { + let w = lObj["w"] as? String ?? "" + let wp = lObj["wp"] as? String ?? "" + + if wp == "p" { + punctuationCount += 1 + } else { + wordCount += 1 + } + resultTextTemp += w + } + } + } + } + + if ls && wordCount == 0 && punctuationCount == 1 { + return + } + + let detectedLanguage = detectLanguage(from: resultTextTemp) + + let type = st["type"] as? String + let typeInt = st["type"] as? Int ?? Int(type ?? "") ?? -1 + + if typeInt == 0 { + callback?.onResult(sessionid: currentSessionId, resultTextTemp, detectedLanguage) + currentSessionId = UUID().uuidString + lastIntermediateResult = "" + } else { + lastIntermediateResult = resultTextTemp + callback?.onRecognizing(sessionid: currentSessionId, resultTextTemp, detectedLanguage) + } + } + + private func detectLanguage(from text: String) -> String { + if isAutoDetectLanguage { + if text.range(of: "[\\u4e00-\\u9fa5]", options: .regularExpression) != nil { + return "zh-CN" + } else { + return "en-US" + } + } else { + return currentLanguage + } + } + + private func getWebSocketUrl() -> String { + let baseWsUrl = "wss://office-api-ast-dx.iflyaisol.com/ast/communicate/v1" + + var params: [String: String] = [:] + params["audio_encode"] = "pcm_s16le" + params["lang"] = "autodialect" + params["samplerate"] = "16000" + params["accessKeyId"] = accessKeyId + params["appId"] = appId + params["utc"] = getUtcTime() + + let signature = calculateSignature(params: params) + params["signature"] = signature + + // 手动构建 URL 查询字符串,确保编码一致性 + var queryString = "" + let allowed = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~") + + // 按照参数名的字母顺序排序,虽然 HTTP 协议不强制,但通常是个好习惯,也方便调试 + let sortedParams = params.sorted { $0.key < $1.key } + + for (index, (key, value)) in sortedParams.enumerated() { + if index > 0 { + queryString += "&" + } + + let encodedKey = key.addingPercentEncoding(withAllowedCharacters: allowed) ?? key + let encodedValue = value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value + + queryString += "\(encodedKey)=\(encodedValue)" + } + + return "\(baseWsUrl)?\(queryString)" + } + + private func getUtcTime() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + dateFormatter.timeZone = TimeZone(secondsFromGMT: 8 * 3600) // GMT+8 + return dateFormatter.string(from: Date()) + } + + private func calculateSignature(params: [String: String]) -> String { + let sortedKeys = params.keys.sorted() + var baseStr = "" + var first = true + + let allowed = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~") + + for key in sortedKeys { + if key == "signature" { continue } + guard let value = params[key], !value.isEmpty else { continue } + + if !first { + baseStr += "&" + } + + // 使用严格的编码规则 + let encodedKey = key.addingPercentEncoding(withAllowedCharacters: allowed) ?? key + let encodedValue = value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value + + baseStr += "\(encodedKey)=\(encodedValue)" + first = false + } + + let keyData = accessKeySecret.data(using: .utf8)! + let msgData = baseStr.data(using: .utf8)! + + if #available(iOS 13.0, *) { + let authentication = HMAC.authenticationCode(for: msgData, using: SymmetricKey(data: keyData)) + return Data(authentication).base64EncodedString() + } else { + return "" + } + } +} diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt index f3aa0b9ff..9fb6f1dcb 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt @@ -85,17 +85,6 @@ class BleCommandSender { */ fun notifyWakeupSignalReceived() - /** - * 获取录音文件对象 - * @return 录音文件对象 - */ - fun getRecordFile(): RecordingFile? - - /** - * 获取重新压缩录音文件对象 - * @return 重新压缩录音文件对象 - */ - fun getRecordFile1(): RecordingFile? } private var callback: CommandSenderCallback? = null @@ -183,8 +172,13 @@ class BleCommandSender { } /** - * 处理设备响应数据 - 从BleService移动过来 - * @param data 响应数据 + * 处理设备响应数据(统一兼容失败无长度的场景) + * + * 帧格式通常为: [HEADER(0)=0xBB][CMD(1)][LEN/STATUS(2)][DATA(len)][CRC(最后1字节)] + * - 当 LEN/STATUS 为 0(例如收到 0xA2, 0x00),表示该命令执行失败,直接回调失败并返回。 + * - 正常响应时进行 CRC 校验(若帧长度不足则跳过校验并告警),拷贝数据使用安全长度以避免越界。 + * + * @param data 设备返回的原始字节数组 */ @SuppressLint("StaticFieldLeak") @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @@ -200,63 +194,64 @@ class BleCommandSender { val header = data[0] // 0xBB val cmdType = data[1].toInt() and 0xFF - - // 判断命令失败响应(长度为0) - if (data.size == 4 && data[2].toInt() and 0xFF == 0) { - Log.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败") - // 创建失败结果Map - val resultMap = mapOf( - "success" to false, - "command" to cmdType, - "rawData" to byteArrayOf() - ) - callback?.notifyDeviceInfoReceived(cmdType, resultMap) - // 通知指令发送器处理响应 - onCommandResponse(cmdType) - return - } - - // 正常响应处理 - val length = data[2].toInt() and 0xFF - - // 检查数据长度 - if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC - Log.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") - return + val lenByte = data[2].toInt() and 0xFF + + + + // 正常响应处理:尽量进行 CRC 校验 + var crcOk = true + if (data.size >= 4) { + val payload = data.sliceArray(0 until data.size - 1) + val calculatedCrc = calculateCrc8Maxim(payload, payload.size) + val receivedCrc = data[data.size - 1].toInt() and 0xFF + crcOk = (calculatedCrc.toInt() and 0xFF) == receivedCrc + if (!crcOk) { + Log.e( + TAG, + "设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" + ) + return + } + } else { + Log.w(TAG, "设备响应长度不足以包含CRC,跳过CRC校验") } + // 统一失败判定 + if (lenByte != data.size-4) { + Log.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败(无数据长度)") + val resultMap = mapOf( + "success" to true, + "command" to cmdType, + "rawData" to byteArrayOf(data[2], data[data.size - 2]) // 取第二位和倒数第二位 + ) + callback?.notifyDeviceInfoReceived(cmdType, resultMap) + onCommandResponse(cmdType) + return + } + // 安全提取响应数据,避免越界 + val actualPayloadLen = (data.size - 4).coerceAtLeast(0) + val copyLen = minOf(lenByte, actualPayloadLen) + val responseData = if (copyLen > 0) ByteArray(copyLen) else ByteArray(0) - // 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节 - val payload = data.sliceArray(0 until data.size - 1) - val calculatedCrc = calculateCrc8Maxim(payload, payload.size) - val receivedCrc = data[data.size - 1].toInt() and 0xFF - - if (calculatedCrc.toInt() and 0xFF != receivedCrc) { - Log.e( + if (copyLen < lenByte) { + Log.w( TAG, - "设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" + "响应长度字段(${lenByte})与实际负载长度(${actualPayloadLen})不一致,按实际长度处理" ) - return } - // 提取响应数据 - val responseData = if (length > 0) ByteArray(length) else ByteArray(0) - if (length > 0) { - System.arraycopy(data, 3, responseData, 0, length) + if (copyLen > 0) { + System.arraycopy(data, 3, responseData, 0, copyLen) } Log.d( TAG, "收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${ - responseData.joinToString(", ") { - "0x${ - (it.toInt() and 0xFF).toString(16) - }" - } + responseData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" } }" ) - // 通知指令发送器处理响应 + onCommandResponse(cmdType) - // 根据命令类型处理不同的响应 + when (cmdType) { BleConst.CMD_GET_VERSION -> { val resultMap = mutableMapOf( @@ -376,21 +371,7 @@ class BleCommandSender { else -> "未知状态($codecStatus)" } - // 处理录音文件 - if (codecStatus == BleConst.CODEC_CONTROL_DECODE_ON || - codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY || - codecStatus == BleConst.CODEC_CONTROL_CALL_RECORD_PLAY || - codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON - ) { - callback?.getRecordFile1()?.closeFile() - callback?.getRecordFile1()?.creatingFiles() - callback?.getRecordFile()?.closeFile() - callback?.getRecordFile()?.creatingFiles() - } else if (codecStatus == BleConst.CODEC_CONTROL_CLOSE) { - callback?.getRecordFile()?.closeFile() - callback?.getRecordFile1()?.closeFile() - } - + Log.d(TAG, "收到设备响应,channelMode: $channelMode") // 声道模式描述 diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index b61fb4ef0..5e8e22b56 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -146,6 +146,12 @@ object BleService { private var maxMtu = 517 // 最大MTU值 (512 + 5字节头部) private var isWaitingForWriteCallback = false // 是否正在等待写入回调 + // 通知注册串行化相关变量 + private val notificationSetupQueue: ArrayDeque = ArrayDeque() + private var isDescriptorWriteInProgress = false + private var notificationTargetsTotal = 0 + private var notificationTargetsSucceeded = 0 + // 初始化状态 private var isInitialized = false @@ -219,10 +225,10 @@ object BleService { scanHandler = Handler(Looper.getMainLooper()) recordfile = RecordingFile(this.context) - recordfile!!.fileName = "不拆分" + recordfile!!.fileName = "耳机端" recordfile1 = RecordingFile(this.context) - recordfile1!!.fileName = "重新压缩" + recordfile1!!.fileName = "手机端" // 初始化Opus音频管理器 if (!opusAudioManager.initialize()) { @@ -332,19 +338,7 @@ object BleService { this@BleService.notifyWakeupSignalReceived() } - /** - * 获取录音文件实例 - */ - override fun getRecordFile(): RecordingFile? { - return recordfile - } - - /** - * 获取录音文件实例1 - */ - override fun getRecordFile1(): RecordingFile? { - return recordfile1 - } + }) // 初始化唤醒词升级指令发送器 @@ -809,33 +803,6 @@ object BleService { updateConnectionState(BleConst.STATE_CONNECTED) Log.d(TAG, "成功连接Ble蓝牙设备:${g.device.name}") companionManager?.unregisterBackgroundScan() - // 设置PHY值为2M以提高传输速度 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - try { - // 首先尝试读取当前PHY状态 - val readResult = g.readPhy() - Log.d(TAG, "读取PHY状态请求结果: $readResult") - - // 延迟一段时间后再设置PHY,确保readPhy完成 - Handler(Looper.getMainLooper()).postDelayed({ - try { - val setResult = g.setPreferredPhy( - BluetoothDevice.PHY_LE_2M_MASK, // TX PHY: 2M - BluetoothDevice.PHY_LE_2M_MASK, // RX PHY: 2M - BluetoothDevice.PHY_OPTION_NO_PREFERRED - ) - Log.d(TAG, "设置PHY为2M请求结果: $setResult") - } catch (e: Exception) { - Log.w(TAG, "设置PHY失败: ${e.message}") - } - }, 500) // 延迟500ms - } catch (e: Exception) { - Log.w(TAG, "读取PHY失败: ${e.message},将使用默认1M PHY") - } - } else { - Log.w(TAG, "当前Android版本不支持PHY设置(需要API 26+),使用默认1M PHY") - } - g.discoverServices() } @@ -929,8 +896,11 @@ object BleService { startBytesStatistics() writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE callWriteChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE - //要先设置音频服务的通知,否则接收不到 - // 设置音频服务的通知(如果存在) + // 串行注册通知:等待上一个通道成功后再注册下一个 + notificationSetupQueue.clear() + isDescriptorWriteInProgress = false + notificationTargetsTotal = 0 + notificationTargetsSucceeded = 0 if (callAudioChar != null && audioChar != null) { setupNotifications(g, callAudioChar) setupNotifications(g, audioChar) @@ -939,7 +909,7 @@ object BleService { getDeviceWakeupVersion() }, 1000) // 延迟3秒再请求版本,确保通知描述符写入完成 } else { - Log.w(TAG, "通话音频服务特征未找到") + Log.w(TAG, "通话音频服务特征未完整找到") } setupNotifications(g, notifyChar) @@ -972,12 +942,14 @@ object BleService { // 统计接收到的字节数 bytesReceivedInCurrentSecond += data.size processAudioData(data) + // Log.i(TAG, "lxm--收到音频特征数据, 字节数=${data.size}") } BleConst.CALL_RECEIVE_AUDIO_CHAR_UUID -> { // 统计接收到的字节数 bytesReceivedInCurrentSecond += data.size processAudioData(data) + //Log.i(TAG, "lxm--收到通话音频特征数据, 字节数=${data.size}") } // 通知特征数据(命令和控制) BleConst.NOTIFY_CHAR_UUID -> { @@ -1032,8 +1004,27 @@ object BleService { override fun onDescriptorWrite( g: BluetoothGatt, d: BluetoothGattDescriptor, status: Int ) { - if (status == BluetoothGatt.GATT_SUCCESS) - Log.i(TAG, "通知描述符写入成功: ${d.characteristic.uuid}") + if (d.uuid == BleConst.CLIENT_CHAR_CONFIG_UUID) { + val charUuid = d.characteristic.uuid + if (status == BluetoothGatt.GATT_SUCCESS) { + Log.i(TAG, "通知描述符写入成功: ${charUuid}") + notificationTargetsSucceeded++ + } else { + Log.e(TAG, "通知描述符写入失败: ${charUuid}, status=${status}") + } + + // 当前一次写入完成,移除队首并继续处理下一个 + if (notificationSetupQueue.isNotEmpty() && notificationSetupQueue.first().uuid == charUuid) { + notificationSetupQueue.removeFirst() + } + isDescriptorWriteInProgress = false + // 如果所有通知已处理完毕,且全部成功,则设置PHY + if (notificationSetupQueue.isEmpty()) { + onAllNotificationsProcessed(g) + } else { + processNextNotificationDescriptor(g) + } + } } /** @@ -1091,6 +1082,66 @@ object BleService { } return true } + + /** + * 串行处理通知描述符写入队列 + * 功能:遵循Android BLE单操作约束,确保按顺序开启各通道通知 + * @param g BluetoothGatt实例 + */ + private fun processNextNotificationDescriptor(g: BluetoothGatt) { + if (isDescriptorWriteInProgress) return + val next = notificationSetupQueue.firstOrNull() ?: run { + onAllNotificationsProcessed(g) + return + } + val started = setupNotifications(g, next) + if (started) { + isDescriptorWriteInProgress = true + } else { + // 当前特征无法启动通知,跳过并尝试下一个 + notificationSetupQueue.removeFirst() + processNextNotificationDescriptor(g) + } + } + + /** + * 所有通知通道处理完成后的回调:在全部成功时设置PHY + */ + private fun onAllNotificationsProcessed(g: BluetoothGatt) { + if (notificationTargetsTotal > 0 && notificationTargetsSucceeded == notificationTargetsTotal) { + applyPreferredPhy(g) + } else { + Log.w(TAG, "通知通道未全部注册成功(${notificationTargetsSucceeded}/${notificationTargetsTotal}),跳过PHY设置") + } + } + + /** + * 读取并设置首选PHY为2M(仅API 26+) + */ + private fun applyPreferredPhy(g: BluetoothGatt) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + val readResult = g.readPhy() + Log.d(TAG, "读取PHY状态请求结果: $readResult") + Handler(Looper.getMainLooper()).postDelayed({ + try { + val setResult = g.setPreferredPhy( + BluetoothDevice.PHY_LE_2M_MASK, + BluetoothDevice.PHY_LE_2M_MASK, + BluetoothDevice.PHY_OPTION_NO_PREFERRED + ) + Log.d(TAG, "设置PHY为2M请求结果: $setResult") + } catch (e: Exception) { + Log.w(TAG, "设置PHY失败: ${e.message}") + } + }, 500) + } catch (e: Exception) { + Log.w(TAG, "读取PHY失败: ${e.message},将使用默认1M PHY") + } + } else { + Log.w(TAG, "当前Android版本不支持PHY设置(需要API 26+),使用默认1M PHY") + } + } /** * 设置唤醒词特征的通知功能 @@ -1153,6 +1204,7 @@ object BleService { * @param data 音频数据 (Opus编码) */ private fun processAudioData(data: ByteArray) { + recordfile?.saveAudioDataToWav(data) // 委托给Opus音频管理器处理 opusAudioManager.processAudioData(data) } @@ -1177,6 +1229,8 @@ object BleService { sampleRate: Int = 16000, packetSize: Int = 40 ): Boolean { + recordfile?.creatingFiles(); + return opusAudioManager.startDecodeStream(hasHeader, channel, sampleRate, packetSize) } @@ -1262,6 +1316,7 @@ object BleService { if (audioChunk != null) { // 发送音频数据块并等待回调 + recordfile1?.saveAudioDataToWav(audioChunk) sendAudioChunkWithCallback(audioChunk) Thread.sleep(AUDIO_SEND_INTERVAL) } @@ -1317,7 +1372,7 @@ object BleService { isWaitingForWriteCallback = true callWriteChar?.value = chunk - recordfile1?.saveAudioDataToWav(chunk) + //recordfile1?.saveAudioDataAsUtf8Hex(chunk, true) val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar) @@ -1464,6 +1519,8 @@ object BleService { */ fun closeCodec(): Boolean { Log.i(TAG, "关闭编解码...") + recordfile?.closeFile() + recordfile1?.closeFile() val cmdData = byteArrayOf( BleConst.CODEC_CONTROL_CLOSE.toByte(), BleConst.AUDIO_CHANNEL_STEREO.toByte() @@ -1495,7 +1552,7 @@ object BleService { fun openA2DPDecoder(channelMode: Int = BleConst.AUDIO_CHANNEL_STEREO): Boolean { Log.i(TAG, "打开A2DP解码器...") startOpusEncodeStream() - + recordfile1?.creatingFiles(); startOpusStreamDecoding(false, 2, 16000, 80) val cmdData = byteArrayOf( BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(), diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift index a6914b556..d757e18ca 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift @@ -282,22 +282,11 @@ private var cmdReplyType: UInt8 = 0 // 处理传入的UUID参数 for uuidStr in uuids { - do { - let cbuuid: CBUUID - if uuidStr.count == 4 { - // 短UUID,直接使用 - cbuuid = CBUUID(string: uuidStr) - } else if uuidStr.count == 36 { - // 完整UUID - cbuuid = CBUUID(string: uuidStr) - } else { - os_log("⚠️ 无效的UUID格式: %{public}@", log: logger, type: .default, uuidStr) - continue - } + if let cbuuid = parseCBUUID(from: uuidStr) { serviceUUIDs.append(cbuuid) os_log("✅ 添加扫描过滤器UUID: %{public}@", log: logger, type: .info, uuidStr) - } catch { - os_log("❌ 构造UUID失败: %{public}@, 错误: %{public}@", log: logger, type: .error, uuidStr, error.localizedDescription) + } else { + os_log("⚠️ 无效的UUID格式或字符: %{public}@", log: logger, type: .error, uuidStr) } } @@ -319,6 +308,40 @@ private var cmdReplyType: UInt8 = 0 } return true } + + /// 验证并解析 UUID 字符串为 CBUUID + /// - Parameter uuidStr: UUID 字符串(支持 4 位短 UUID、36 位完整 UUID;扩展支持 32 位不带短横线 UUID) + /// - Returns: 解析成功返回 CBUUID,否则返回 nil + private func parseCBUUID(from uuidStr: String) -> CBUUID? { + let trimmed = uuidStr.trimmingCharacters(in: .whitespacesAndNewlines) + + // 4 位短 UUID:仅允许十六进制字符,通过正则校验 + if trimmed.count == 4 { + if trimmed.range(of: "^[0-9a-fA-F]{4}$", options: .regularExpression) != nil { + return CBUUID(string: trimmed.lowercased()) + } else { + return nil + } + } + + // 36 位完整 UUID:使用系统 UUID 进行格式校验 + if trimmed.count == 36, let nsuuid = UUID(uuidString: trimmed) { + return CBUUID(nsuuid: nsuuid) + } + + // 32 位完整 UUID(不带短横线):按 8-4-4-4-12 格式插入短横线后校验 + if trimmed.count == 32, + trimmed.range(of: "^[0-9a-fA-F]{32}$", options: .regularExpression) != nil { + let lower = trimmed.lowercased() + let formatted = "\(lower.prefix(8))-\(lower.dropFirst(8).prefix(4))-\(lower.dropFirst(12).prefix(4))-\(lower.dropFirst(16).prefix(4))-\(lower.dropFirst(20))" + if let nsuuid = UUID(uuidString: formatted) { + return CBUUID(nsuuid: nsuuid) + } + } + + // 其他长度不支持 + return nil + } /// 停止扫描 public func stopScan() { @@ -868,22 +891,27 @@ private var cmdReplyType: UInt8 = 0 } /// 处理接收到的响应数据 + /// + /// - 功能: + /// - 解析设备响应帧,校验头、长度与 CRC + /// - 与 Android 保持一致的失败判定:当长度字段与实际长度不匹配时,上报 rawData = [length, data[data.count-2]] + /// - 在 CMD_CONTROL_CODEC 分支对齐安卓对错误响应的处理逻辑 private func handleResponseData(_ data: Data) { - guard let characteristic = writeCharacteristic, + guard let characteristic = writeCharacteristic, connectionState == BleConst.STATE_CONNECTED else { os_log(" 设备未连接", log: logger, type: .error) - return + return } guard data.count >= 4 else { os_log("响应数据格式错误:数据长度过短", log: logger, type: .error) return } - + // 提取帧信息 let header = data[0] let cmd = data[1] let length = data[2] - + // 检查帧头,确保是设备响应 guard header == BleConst.DEVICE_RESPONSE_HEADER else { // 如果是设备主动上报 @@ -892,35 +920,59 @@ private var cmdReplyType: UInt8 = 0 } return } - + // 记录响应详情 var hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ") os_log("收到设备响应: %@", log: logger, type: .debug, hexString) - - // 检查数据长度 + + // 检查数据长度(与 Android 统一失败判定保持一致) if data.count != Int(length) + 4 { // 帧头 + 命令 + 长度 + 数据 + CRC os_log("设备响应数据长度不匹配: 期望=%d, 实际=%d", log: logger, type: .error, Int(length) + 4, data.count) + + // 与 Android 保持一致:构造 rawData 为 [length, 倒数第二位(payload 最后一位)] + var raw: [UInt8] = [] + raw.append(length) + if data.count >= 2 { + raw.append(data[data.count - 2]) + } + + let resultMap: [String: Any] = [ + "success": true, // 与 Android 对齐(Dart 层按 rawData 判断) + "command": Int(cmd), + "rawData": Data(raw) + ] + + // 推进命令队列(与后续正常路径一致) + isReply = true + cmdReplyType = cmd + if !commandQueue.isEmpty { + let nextCmd = commandQueue.removeFirst() + peripheralDevice?.writeValue(nextCmd, for: characteristic, type: .withoutResponse) + } + + // 上报到 Dart 层 + notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) return } - + // 计算CRC校验 - 不包括最后一个字节(CRC) let payload = data.prefix(data.count - 1) let calculatedCrc = calculateCRC8(data: payload) let receivedCrc = data[data.count - 1] - + if calculatedCrc != receivedCrc { os_log("设备响应CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc) return } - + // 提取响应数据 let responseData = length > 0 ? data.subdata(in: 3..<(3 + Int(length))) : Data() - isReply = true; - cmdReplyType = cmd; - if !commandQueue.isEmpty { - let nextCmd = commandQueue.removeFirst() - peripheralDevice?.writeValue(nextCmd, for: characteristic, type: .withoutResponse) -} + isReply = true + cmdReplyType = cmd + if !commandQueue.isEmpty { + let nextCmd = commandQueue.removeFirst() + peripheralDevice?.writeValue(nextCmd, for: characteristic, type: .withoutResponse) + } // 根据命令类型处理设备响应 switch cmd { @@ -930,103 +982,98 @@ private var cmdReplyType: UInt8 = 0 "command": "versionInfo", "rawData": responseData ] - + if !responseData.isEmpty { - // 如果无法解析为字符串,以点分隔的数字表示版本 let versionNums = responseData .map { $0 } - .filter { $0 != 0xFF } // 过滤掉无效字节 + .filter { $0 != 0xFF } .map { String($0) } .joined(separator: ".") resultMap["version"] = versionNums os_log("接收到目标版本号(数字格式): %@", log: logger, type: .info, versionNums) } - + notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) - + case BleConst.CMD_GET_PRODUCT_ID: var resultMap: [String: Any] = [ "success": true, "command": "productId", "rawData": responseData ] - + if !responseData.isEmpty { let productId = responseData.map { String($0 & 0xFF) }.joined(separator: ".") resultMap["productId"] = productId os_log("接收到产品ID: %d", log: logger, type: .info, productId) } - + notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) - + case BleConst.CMD_GET_COLOR_ID: var resultMap: [String: Any] = [ "success": true, "command": "colorInfo", "rawData": responseData ] - + if !responseData.isEmpty { let colorId = Int(responseData[0]) resultMap["colorId"] = colorId os_log("接收到耳机颜色ID: %d", log: logger, type: .info, colorId) } - + notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) - + case BleConst.CMD_GET_BATTERY_INFO: var resultMap: [String: Any] = [ "success": true, "command": "batteryInfo", "rawData": responseData ] - + if responseData.count >= 3 { - // 处理电池信息 - 左耳、右耳、充电仓 let leftEarBattery = Int(responseData[0] & 0x7F) let leftEarCharging = (responseData[0] & 0x80) != 0 - + let rightEarBattery = Int(responseData[1] & 0x7F) let rightEarCharging = (responseData[1] & 0x80) != 0 - + let caseBattery = Int(responseData[2] & 0x7F) let caseCharging = (responseData[2] & 0x80) != 0 - - // 添加到结果Map + resultMap["leftEarBattery"] = leftEarBattery resultMap["leftEarCharging"] = leftEarCharging resultMap["rightEarBattery"] = rightEarBattery resultMap["rightEarCharging"] = rightEarCharging resultMap["caseBattery"] = caseBattery resultMap["caseCharging"] = caseCharging - resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 - - os_log("接收到电量信息: 左耳=%d%% (%@), 右耳=%d%% (%@), 充电仓=%d%% (%@)", + resultMap["batteryLevel"] = leftEarBattery + + os_log("接收到电量信息: 左耳=%d%% (%@), 右耳=%d%% (%@), 充电仓=%d%% (%@)", log: logger, type: .info, - leftEarBattery, leftEarCharging ? "充电中" : "未充电", + leftEarBattery, leftEarCharging ? "充电中" : "未充电", rightEarBattery, rightEarCharging ? "充电中" : "未充电", caseBattery, caseCharging ? "充电中" : "未充电") } else if !responseData.isEmpty { - // 兼容单字节电量 let batteryLevel = Int(responseData[0]) resultMap["batteryLevel"] = batteryLevel os_log("接收到电量信息(简化): %d%%", log: logger, type: .info, batteryLevel) } - + notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) - + case BleConst.CMD_CONTROL_CODEC: var resultMap: [String: Any] = [ "success": true, "command": Int(cmd), "rawData": responseData ] - - if responseData.count >= 2 { + + if !responseData.isEmpty && responseData.count >= 2 { let codecStatus = Int(responseData[0]) let channelMode = Int(responseData[1]) - - // 解码状态描述 + let statusDesc: String switch codecStatus { case Int(BleConst.CODEC_CONTROL_CLOSE): @@ -1042,8 +1089,7 @@ private var cmdReplyType: UInt8 = 0 default: statusDesc = "未知状态(\(codecStatus))" } - - // 声道模式描述 + let channelDesc: String switch channelMode { case Int(BleConst.AUDIO_CHANNEL_LEFT): @@ -1055,19 +1101,29 @@ private var cmdReplyType: UInt8 = 0 default: channelDesc = "未知声道(\(channelMode))" } - + resultMap["codecStatus"] = codecStatus resultMap["channelMode"] = channelMode resultMap["statusDesc"] = statusDesc resultMap["channelDesc"] = channelDesc - + os_log("编解码控制响应: %@, %@", log: logger, type: .info, statusDesc, channelDesc) + } else if !responseData.isEmpty && responseData.count == 1 { + // 与 Android 保持一致:单字节且为 0 判定为错误 + let codecStatus = Int(responseData[0]) + if codecStatus == 0x00 { + os_log("收到设备响应: 失败, codecStatus: %d", log: logger, type: .debug, codecStatus) + resultMap = [ + "success": true, // 与 Android 对齐(Dart 层按 rawData 判断) + "command": "Error", + "rawData": responseData + ] + } } - + notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) - + default: - // 对于未知命令,返回原始数据 let resultMap: [String: Any] = [ "success": true, "command": Int(cmd), diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt index e0374ae37..7be026f4c 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -38,7 +38,7 @@ interface StreamCallback { /** * AI使用量 * */ - fun onUsage(sessionid: String,prompt_tokens: Int?,completion_tokens: Int?,total_tokens: Int?) + fun onUsage(sessionid: String,prompt_tokens: Int?,completion_tokens: Int?,callfunctions: List?) /** * 接收到新的token */ @@ -106,6 +106,13 @@ private data class ToolCallInfo( } } +// 会话统计信息结构 +private data class SessionStats( + var promptTokens: Int = 0, + var completionTokens: Int = 0, + val calledFunctions: MutableList = mutableListOf() +) + /** * ChatAPI服务的Android实现 * 基于 openai-kotlin 4.0.1,与 iOS 版本接口完全一致 @@ -113,7 +120,7 @@ private data class ToolCallInfo( class ChatApiService(private val context: android.content.Context? = null) : CoroutineScope { override val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob() - + companion object { private const val TAG = "ChatApiService" } @@ -133,7 +140,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // MCP 客户端 private var _mcpClient: MCPClient? = null - + // 公开的MCP客户端访问器 val mcpClient: MCPClient? get() = _mcpClient @@ -147,6 +154,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor private var currSessionId = "" // 为每个会话维护独立的回调函数 private val sessionCallbacks: MutableMap = Collections.synchronizedMap(mutableMapOf()) + // 会话统计信息 + private val sessionStats: MutableMap = Collections.synchronizedMap(mutableMapOf()) @@ -165,8 +174,17 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // 1. 取消当前的协程任务 currentStreamJob?.cancel(CancellationException("New session started")) currentStreamJob = null - - // 2. 通知旧会话被中止 + if (oldSessionId != ""){ + val stats = sessionStats[oldSessionId] + getSessionCallback(oldSessionId)?.onUsage( + oldSessionId, + stats?.promptTokens, + stats?.completionTokens, + stats?.calledFunctions?.toList() + ) + sessionStats.remove(oldSessionId) + } + getSessionCallback(oldSessionId)?.onComplete(oldSessionId) // 直接完成当前会话 // 3. 清理状态 @@ -174,6 +192,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor sessionCallbacks.remove(oldSessionId) toolCalls.clear() + Log.d(TAG, "会话 $oldSessionId 已被中止") } } @@ -187,6 +206,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor sessionCallbacks.remove(oldSessionId) currentStreamJob = null toolCalls.clear() + sessionStats.remove(oldSessionId) } /** @@ -327,14 +347,18 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor /** * 发送消息(流式输出)- 兼容JSONArray格式 */ - fun sendMessageStream(sessionid: String,messages: org.json.JSONArray, callback: StreamCallback) { + fun sendMessageStream(sessionid: String,vipexptime: Int, messages: org.json.JSONArray, callback: StreamCallback) { // 1. 立即中止旧会话 abortCurrentSession() - - // 2. 创建新会话 -// val newSessionId = UUID.randomUUID().toString() + // 判断时间是否过期:如果 vipexptime 小于当前时间戳(秒),则视为过期 + if (vipexptime <= (System.currentTimeMillis() / 1000)) { + callback.onError(sessionid,2001,"会员过期") + return + } currSessionId = sessionid sessionCallbacks[sessionid] = callback + // 初始化会话统计 + sessionStats[sessionid] = SessionStats() // 3. 转换消息并开始处理 val messageList = convertJsonArrayToList(messages) @@ -414,7 +438,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor throw IllegalStateException("OpenAI客户端未初始化") } // Log.e("ChatApiService", "liwei---------,发送AI请求 model:${currentModel} messages:${chatMessages}") - // 添加超时控制,10秒超时 + // 添加超时控制,10秒超时 val flow = withTimeout(10.seconds) { openAI!!.chatCompletions(chatCompletionRequest) } @@ -496,7 +520,6 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } toolCall.function?.let { function -> - // 安全处理函数名称 - 使用 try-catch 避免 null 属性访问 try { val functionName = function.name if (!functionName.isNullOrEmpty()) { @@ -532,9 +555,22 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } } } - val promptTokens = estimatePromptTokens(chatMessages); //计算上传token - val completionTokens = roughTokenCount(replyContext); //计算上传token - getSessionCallback(sessionid)?.onUsage(sessionid,promptTokens,completionTokens,promptTokens+completionTokens) + + // 仅统计数据字节数,服务端负责计算token + val promptBytes = try { + val jsonArray = org.json.JSONArray() + messages.forEach { msg -> + jsonArray.put(convertMapToJsonObject(msg)) + } + jsonArray.toString().toByteArray(Charsets.UTF_8).size + } catch (_: Exception) { 0 } + val completionBytes = replyContext.toByteArray(Charsets.UTF_8).size + // 累计到会话统计 + sessionStats.getOrPut(sessionid) { SessionStats() }.apply { + this.promptTokens += promptBytes + this.completionTokens += completionBytes + } + // Log.d(TAG, "liwei-------------------------开始AI 对话 8") // 完成处理 if (sessionid == currSessionId) { @@ -543,12 +579,33 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor val hasToolCalls = processToolCalls(sessionid) if (!hasToolCalls) { try { + val stats = sessionStats[sessionid] + getSessionCallback(sessionid)?.onUsage( + sessionid, + stats?.promptTokens, + stats?.completionTokens, + stats?.calledFunctions?.toList() + ) getSessionCallback(sessionid)?.onComplete(sessionid) } catch (e: Exception) { Log.e(TAG, "onComplete回调异常: ${e.message}", e) } clearCurrentSession() } + }else{ + // 会话结束,上报使用统计,再回调完成 + try { + val stats = sessionStats[sessionid] + getSessionCallback(sessionid)?.onUsage( + sessionid, + stats?.promptTokens, + stats?.completionTokens, + stats?.calledFunctions?.toList() + ) + getSessionCallback(sessionid)?.onComplete(sessionid) + } catch (e: Exception) { + Log.e(TAG, "onUsage回调异常: ${e.message}", e) + } } // Log.d(TAG, "liwei-------------------------开始AI 对话 10") } catch (e: CancellationException) { @@ -557,6 +614,13 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } catch (e: Exception) { if (sessionid == currSessionId) { try { + val stats = sessionStats[sessionid] + getSessionCallback(sessionid)?.onUsage( + sessionid, + stats?.promptTokens, + stats?.completionTokens, + stats?.calledFunctions?.toList() + ) Log.d(TAG, "AI聊天异常 Session $sessionid 错误类型:${e::class.simpleName} 错误完整类型:${e::class.qualifiedName}") getSessionCallback(sessionid)?.onError(sessionid,1000,"流式请求失败: ${e.message}") } catch (ex: Exception) { @@ -586,7 +650,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // 只处理有效的工具调用 val validToolCalls = toolCalls.values.filter { it.isValid() } - if (validToolCalls.isEmpty()) { + if (validToolCalls.isEmpty()) { Log.w("ChatApiService", "[Session: $sessionid] 没有有效的工具调用(参数可能不完整)") return false } @@ -601,6 +665,12 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor "id" to firstToolCall.id ) + // 记录方法名称以返回给调用方 + val functionName: String = firstToolCall.name + if (functionName.isNotEmpty()) { + sessionStats.getOrPut(sessionid) { SessionStats() }.calledFunctions.add(functionName) + } + // 通知上层工具调用事件 try { getSessionCallback(sessionid)?.onFunctionCall(sessionid,convertMapToJsonObject(functionCall)) @@ -609,7 +679,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } // 在当前协程作用域内处理工具调用,使用async确保生命周期管理 - val toolCallDeferred = async { + val toolCallDeferred = async { try { // 检查会话是否仍然有效 if (sessionid != currSessionId) { @@ -750,7 +820,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // 不重新抛出异常,让流程正常结束 } - return true + return true } /** @@ -983,7 +1053,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor * 关闭MCP客户端 */ fun closeMcpClient() { - runBlocking { + runBlocking { _mcpClient?.disconnectAll() _mcpClient = null } @@ -1230,7 +1300,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor ChatMessage.System(displayText) } Role.User -> ChatMessage.User(contentParts) - Role.Assistant -> { + Role.Assistant -> { // 将多模态内容转换为ListContent val listContent = ListContent(contentParts) // 提取文本内容作为Assistant消息的content @@ -1468,4 +1538,4 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor return "Function '$functionName' called with arguments: $arguments" } } -} \ No newline at end of file +} diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt index 94173554d..bb16bc993 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt @@ -10,12 +10,10 @@ import android.content.pm.PackageManager import android.location.LocationManager import android.media.AudioManager import android.net.Uri -import android.os.Build import android.provider.AlarmClock import android.provider.CalendarContract import android.provider.ContactsContract import android.provider.Settings -import android.telephony.SmsManager import android.util.Log import androidx.core.content.ContextCompat import com.yunqiinnovation.location_service.LocationService @@ -519,16 +517,6 @@ private class SendTextMessageHandler(private val context: Context?) : FunctionHa val handler = context.let { SystemFunctionHandler(it) } return try { - // 检查发送短信权限 - if (!handler.checkPermissionAndGuide(Manifest.permission.SEND_SMS, "发送短信", "发送短信")) { - return JSONObject().apply { - put("error", JSONObject().apply { - put("code", "permission_denied") - put("message", "需要短信权限才能发送短信,请在权限管理中开启短信权限") - }) - }.toString() - } - val phoneNumber = handler.getPhoneNumberForContact(phone_number) if (phoneNumber.isEmpty()) { return JSONObject().apply { @@ -539,24 +527,24 @@ private class SendTextMessageHandler(private val context: Context?) : FunctionHa }.toString() } - // 使用SmsManager发送短信 - val smsManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - context.getSystemService(SmsManager::class.java) - } else { - @Suppress("DEPRECATION") - SmsManager.getDefault() + val intent = Intent(Intent.ACTION_SENDTO).apply { + data = Uri.parse("smsto:$phoneNumber") + putExtra("sms_body", message) + flags = Intent.FLAG_ACTIVITY_NEW_TASK } - - // 处理长短信 - val parts = smsManager.divideMessage(message) - if (parts.size == 1) { - smsManager.sendTextMessage(phoneNumber, null, message, null, null) + if (context.packageManager?.queryIntentActivities(intent, 0)?.isNotEmpty() == true) { + context.startActivity(intent) } else { - smsManager.sendMultipartTextMessage(phoneNumber, null, parts, null, null) + return JSONObject().apply { + put("error", JSONObject().apply { + put("code", "no_sms_app") + put("message", "未找到可用的短信应用") + }) + }.toString() } JSONObject().apply { - put("result", "已成功向${phone_number}发送短信") + put("result", "已打开短信应用,请确认发送") put("phone", phoneNumber) put("message", message) }.toString() diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt index d303ae3b4..49e3b8408 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt @@ -8,9 +8,6 @@ import android.net.Uri import android.provider.Settings import android.util.Log import androidx.core.content.ContextCompat -import com.aallam.openai.api.chat.ChatMessage -import com.aallam.openai.api.chat.TextContent -import com.aallam.openai.api.chat.ListContent import io.ktor.client.* import io.ktor.client.engine.okhttp.* import io.ktor.client.plugins.sse.* @@ -166,47 +163,4 @@ fun roughTokenCount(text: String): Int { val englishCharCount = trimmed.length - chineseCharCount return chineseCharCount + (englishCharCount / 4) + 1 -} - - -/** - * 估算消息列表的总 token 数量(近似) - * @param messages List of message map: role: user/assistant, content: string or array - * @return 估算 token 数 - */ -fun estimatePromptTokens(messages: List): Int { - var totalText = "" - for (message in messages) { - when (val content = message.messageContent) { - is TextContent? -> { - totalText += content?.content + " " - } - is ListContent -> { - // 处理多模态内容数组 - content.content.forEach { item -> - if (item is Map<*, *>) { - val type = item["type"] as? String - if (type == "text") { - val text = item["text"] as? String ?: "" - totalText += text + " " - }else if(type == "image_url"){ - val text = item["image_url"] as? String ?: "" - totalText += text + " " - } - } - } - } - } - } - return roughTokenCount(totalText) -} -//fun estimatePromptTokens(messages: List): Int { -// var tokens = 0 -// for (msg in messages) { -// tokens += 4 // 每条消息结构开销 -// tokens += roughTokenCount(msg.role.role.lowercase()) // ChatRole 枚举转小写字符串 -// tokens += roughTokenCount(msg.content ?: "") -// } -// tokens += 2 // 结尾 assistant 回复结构开销 -// return tokens -//} \ No newline at end of file +} \ No newline at end of file diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift index c37e74bae..58a940cc1 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift @@ -271,12 +271,13 @@ public class ChatApiService: NSObject { throw ChatApiException("无效的响应格式") } - /// 发送消息(流式输出) - // MARK: - 流式请求实现 -public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) { +/// 发送消息(流式输出) + // MARK: - 流式请求实现 +public func sendMessageStream(_ sessionId: String,vipexptime: Int, messages: [[String: Any]]) { // 1. 立即中止旧会话 abortCurrentSession() + // 2. 创建新会话 currSessionId = sessionId @@ -289,6 +290,11 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) { getSessionCallback(sessionId)?.onError(sessionId,1000,"当前网络不可用") return } + let nowSec = Int(Date().timeIntervalSince1970) + if vipexptime <= 0 || nowSec >= vipexptime { + getSessionCallback(sessionId)?.onError(sessionId,2001,"会员已过期") + return + } // 保存当前消息和回调 currentMessages = messages @@ -400,7 +406,7 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) { self.getSessionCallback(sessionId)?.onUsage(sessionId, promptTokens, completionTokens, totalTokens) // 处理工具调用 - let hasToolCalls = await self.processToolCalls(sessionId) + let hasToolCalls = await self.processToolCalls(sessionId,vipexptime: vipexptime) if !hasToolCalls { self.getSessionCallback(sessionId)?.onComplete(sessionId) self.clearCurrentSession() @@ -444,7 +450,7 @@ private func roughTokenCount(text: String) -> Int { } /// 处理工具调用 -private func processToolCalls(_ sessionId:String) async -> Bool { +private func processToolCalls(_ sessionId:String,vipexptime: Int) async -> Bool { // 验证工具调用集合不为空 if toolCalls.isEmpty { print("[Session: \(sessionId)] 没有工具调用需要处理") @@ -563,6 +569,7 @@ private func processToolCalls(_ sessionId:String) async -> Bool { // 将结果发送回OpenAI继续对话 await self.sendFunctionCallResultInternal( sessionId: sessionId, + vipexptime: vipexptime, messages: self.currentMessages, functionCall: functionCall, functionResult: self.jsonToString(result) ?? "{}" @@ -575,6 +582,7 @@ private func processToolCalls(_ sessionId:String) async -> Bool { let errorMessage = "工具调用处理失败: \(error.localizedDescription)" await self.sendFunctionCallResultInternal( sessionId: sessionId, + vipexptime: vipexptime, messages: self.currentMessages, functionCall: functionCall, functionResult: errorMessage @@ -623,6 +631,7 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] { /// 发送函数调用结果(内部方法) private func sendFunctionCallResultInternal( sessionId:String, + vipexptime: Int, messages: [[String: Any]], functionCall: [String: Any], functionResult: String @@ -656,7 +665,7 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] { ]) // 重新发起对话,让AI基于工具调用结果生成回复 - sendMessageStream(sessionId,messages: fullMessages) + sendMessageStream(sessionId,vipexptime: vipexptime,messages: fullMessages) } /// 取消当前流式请求 @@ -1090,4 +1099,3 @@ private class LocalFunctionHandler: FunctionHandler { return "LOCAL_FUNCTION:\(functionName)" } } - diff --git a/local_plugins/music_service/lib/music_service.dart b/local_plugins/music_service/lib/music_service.dart index b7007500c..61860bbf3 100644 --- a/local_plugins/music_service/lib/music_service.dart +++ b/local_plugins/music_service/lib/music_service.dart @@ -1,157 +1,2 @@ -import 'dart:ffi'; - -import 'package:flutter/services.dart'; - -class MusicService { - static final MusicService _instance = MusicService._internal(); - - /// 获取单例 - factory MusicService() => _instance; - - // 方法通道 - final MethodChannel _methodChannel = const MethodChannel( - 'com.yunqiinnovation.music_service', - ); - - // 事件通道 - 简化为只使用数据和状态通道 - late final EventChannel _dataEventChannel; - late final EventChannel _statusEventChannel; - - // 事件流 - late final Stream _dataStream; - late final Stream _statusStream; - - // 获取事件流 - Stream get onDataReceived => _dataStream; - - // 状态流直接传递原始数据 - Stream get onStatusUpdated => _statusStream; - - // 内部构造函数 - MusicService._internal() { - _dataEventChannel = const EventChannel( - 'com.yunqiinnovation.music_service/data_events', - ); - _statusEventChannel = const EventChannel( - 'com.yunqiinnovation.music_service/status_events', - ); - - // 初始化事件流 - _dataStream = _dataEventChannel.receiveBroadcastStream(); - _statusStream = _statusEventChannel.receiveBroadcastStream(); - } - - Future playMusic(Map song) async { - await _methodChannel.invokeMethod('play', {'song': song}); - } - - Future pauseMusic() async => _methodChannel.invokeMethod('pause'); - Future resumeMusic() async => _methodChannel.invokeMethod('resume'); - Future nextTrack() async => _methodChannel.invokeMethod('next'); - Future previousTrack() async => _methodChannel.invokeMethod('previous'); - Future stopMusic() async => _methodChannel.invokeMethod('stop'); - Future setVolume(double volume) async => _methodChannel.invokeMethod( - 'setVolume', - {'volume': volume}, - ); // volume: 0.0~1.0 - Future> getStatus() async { - final result = await _methodChannel.invokeMethod('getStatus'); - return { - 'isPlaying': result?['isPlaying'] ?? false, - 'id': result?['id'] ?? 0, - }; - } - - /// 获取播放进度信息 - Future> getProgress() async { - final result = await _methodChannel.invokeMethod('getProgress'); - return { - 'currentPosition': result?['currentPosition'] ?? 0, // 当前播放位置(毫秒) - 'duration': result?['duration'] ?? 0, // 总时长(毫秒) - 'progress': result?['progress'] ?? 0.0, // 播放进度(0.0-1.0) - }; - } - - /// 跳转到指定位置 - Future seekTo(int positionMs) async { - await _methodChannel.invokeMethod('seekTo', {'position': positionMs}); - } - - /// 播放音乐列表 - Future playMusicPlaylist(List> songs) async { - await _methodChannel.invokeMethod('playlist', {'songs': songs}); - } - - /// 更新媒体会话进度信息 - Future updateMediaSessionProgress( - int currentPosition, - int duration, - double progress, - ) async { - await _methodChannel.invokeMethod('updateMediaSessionProgress', { - 'currentPosition': currentPosition, - 'duration': duration, - 'progress': progress, - }); - } - - /// 设置媒体会话回调 - Future setMediaSessionCallbacks({ - required Function() onPlay, - required Function() onPause, - required Function() onNext, - required Function() onPrevious, - required Function(int) onSeekTo, - required Function() onStop, - required Function(int, int, int, double) onProgress, - }) async { - // 设置方法调用处理器来处理来自原生平台的回调 - _methodChannel.setMethodCallHandler((call) async { - switch (call.method) { - case 'onMediaSessionPlay': - onPlay(); - break; - case 'onMediaSessionPause': - onPause(); - break; - case 'onMediaSessionNext': - onNext(); - break; - case 'onMediaSessionPrevious': - onPrevious(); - break; - case 'onMediaSessionSeekTo': - final position = call.arguments['position'] as int; - onSeekTo(position); - break; - case 'onMediaSessionStop': - onStop(); - break; - case 'onMediaSessionProgress': - final currentIdInternal = call.arguments['currentIdInternal'] as int; - final currentPosition = call.arguments['currentPosition'] as int; - final duration = call.arguments['duration'] as int; - final progress = call.arguments['progress'] as double; - onProgress(currentIdInternal, currentPosition, duration, progress); - break; - } - }); - - // 通知原生平台启用媒体会话回调 - await _methodChannel.invokeMethod('enableMediaSessionCallbacks'); - } - - /// 更新媒体会话元数据 - Future updateMediaSessionMetadata(Map song) async { - await _methodChannel.invokeMethod('updateMediaSessionMetadata', { - 'song': song, - }); - } - - /// 更新媒体会话播放状态 - Future updateMediaSessionPlaybackState(bool isPlaying) async { - await _methodChannel.invokeMethod('updateMediaSessionPlaybackState', { - 'isPlaying': isPlaying, - }); - } -} +export 'music_service_native.dart' + if (dart.library.html) 'music_service_stub.dart'; diff --git a/local_plugins/music_service/lib/music_service_native.dart b/local_plugins/music_service/lib/music_service_native.dart new file mode 100644 index 000000000..b7007500c --- /dev/null +++ b/local_plugins/music_service/lib/music_service_native.dart @@ -0,0 +1,157 @@ +import 'dart:ffi'; + +import 'package:flutter/services.dart'; + +class MusicService { + static final MusicService _instance = MusicService._internal(); + + /// 获取单例 + factory MusicService() => _instance; + + // 方法通道 + final MethodChannel _methodChannel = const MethodChannel( + 'com.yunqiinnovation.music_service', + ); + + // 事件通道 - 简化为只使用数据和状态通道 + late final EventChannel _dataEventChannel; + late final EventChannel _statusEventChannel; + + // 事件流 + late final Stream _dataStream; + late final Stream _statusStream; + + // 获取事件流 + Stream get onDataReceived => _dataStream; + + // 状态流直接传递原始数据 + Stream get onStatusUpdated => _statusStream; + + // 内部构造函数 + MusicService._internal() { + _dataEventChannel = const EventChannel( + 'com.yunqiinnovation.music_service/data_events', + ); + _statusEventChannel = const EventChannel( + 'com.yunqiinnovation.music_service/status_events', + ); + + // 初始化事件流 + _dataStream = _dataEventChannel.receiveBroadcastStream(); + _statusStream = _statusEventChannel.receiveBroadcastStream(); + } + + Future playMusic(Map song) async { + await _methodChannel.invokeMethod('play', {'song': song}); + } + + Future pauseMusic() async => _methodChannel.invokeMethod('pause'); + Future resumeMusic() async => _methodChannel.invokeMethod('resume'); + Future nextTrack() async => _methodChannel.invokeMethod('next'); + Future previousTrack() async => _methodChannel.invokeMethod('previous'); + Future stopMusic() async => _methodChannel.invokeMethod('stop'); + Future setVolume(double volume) async => _methodChannel.invokeMethod( + 'setVolume', + {'volume': volume}, + ); // volume: 0.0~1.0 + Future> getStatus() async { + final result = await _methodChannel.invokeMethod('getStatus'); + return { + 'isPlaying': result?['isPlaying'] ?? false, + 'id': result?['id'] ?? 0, + }; + } + + /// 获取播放进度信息 + Future> getProgress() async { + final result = await _methodChannel.invokeMethod('getProgress'); + return { + 'currentPosition': result?['currentPosition'] ?? 0, // 当前播放位置(毫秒) + 'duration': result?['duration'] ?? 0, // 总时长(毫秒) + 'progress': result?['progress'] ?? 0.0, // 播放进度(0.0-1.0) + }; + } + + /// 跳转到指定位置 + Future seekTo(int positionMs) async { + await _methodChannel.invokeMethod('seekTo', {'position': positionMs}); + } + + /// 播放音乐列表 + Future playMusicPlaylist(List> songs) async { + await _methodChannel.invokeMethod('playlist', {'songs': songs}); + } + + /// 更新媒体会话进度信息 + Future updateMediaSessionProgress( + int currentPosition, + int duration, + double progress, + ) async { + await _methodChannel.invokeMethod('updateMediaSessionProgress', { + 'currentPosition': currentPosition, + 'duration': duration, + 'progress': progress, + }); + } + + /// 设置媒体会话回调 + Future setMediaSessionCallbacks({ + required Function() onPlay, + required Function() onPause, + required Function() onNext, + required Function() onPrevious, + required Function(int) onSeekTo, + required Function() onStop, + required Function(int, int, int, double) onProgress, + }) async { + // 设置方法调用处理器来处理来自原生平台的回调 + _methodChannel.setMethodCallHandler((call) async { + switch (call.method) { + case 'onMediaSessionPlay': + onPlay(); + break; + case 'onMediaSessionPause': + onPause(); + break; + case 'onMediaSessionNext': + onNext(); + break; + case 'onMediaSessionPrevious': + onPrevious(); + break; + case 'onMediaSessionSeekTo': + final position = call.arguments['position'] as int; + onSeekTo(position); + break; + case 'onMediaSessionStop': + onStop(); + break; + case 'onMediaSessionProgress': + final currentIdInternal = call.arguments['currentIdInternal'] as int; + final currentPosition = call.arguments['currentPosition'] as int; + final duration = call.arguments['duration'] as int; + final progress = call.arguments['progress'] as double; + onProgress(currentIdInternal, currentPosition, duration, progress); + break; + } + }); + + // 通知原生平台启用媒体会话回调 + await _methodChannel.invokeMethod('enableMediaSessionCallbacks'); + } + + /// 更新媒体会话元数据 + Future updateMediaSessionMetadata(Map song) async { + await _methodChannel.invokeMethod('updateMediaSessionMetadata', { + 'song': song, + }); + } + + /// 更新媒体会话播放状态 + Future updateMediaSessionPlaybackState(bool isPlaying) async { + await _methodChannel.invokeMethod('updateMediaSessionPlaybackState', { + 'isPlaying': isPlaying, + }); + } +} diff --git a/local_plugins/music_service/lib/music_service_stub.dart b/local_plugins/music_service/lib/music_service_stub.dart new file mode 100644 index 000000000..0393682dd --- /dev/null +++ b/local_plugins/music_service/lib/music_service_stub.dart @@ -0,0 +1,38 @@ +class MusicService { + static final MusicService _instance = MusicService._internal(); + factory MusicService() => _instance; + + MusicService._internal(); + + Future playMusic(Map song) async {} + Future playMusicPlaylist(List> playlist) async {} + Future pauseMusic() async {} + Future resumeMusic() async {} + Future stopMusic() async {} + Future setVolume(double volume) async {} + Future> getStatus() async => {}; + Future> getProgress() async => { + 'isPlaying': false, + 'currentPosition': 0, + 'duration': 0, + 'progress': 0.0, + }; + Future seekTo(int positionMs) async {} + + Future updateMediaSessionProgress( + int currentPosition, int duration, double progress) async {} + + Future setMediaSessionCallbacks({ + required Function() onPlay, + required Function() onPause, + required Function() onNext, + required Function() onPrevious, + required Function(int) onSeekTo, + required Function() onStop, + required Function(int, int, int, double) onProgress, + }) async {} + + Future updateMediaSessionMetadata(Map song) async {} + + Future updateMediaSessionPlaybackState(bool isPlaying) async {} +} diff --git a/local_plugins/native_plugin/.gitignore b/local_plugins/native_plugin/.gitignore new file mode 100644 index 000000000..e7d347d9d --- /dev/null +++ b/local_plugins/native_plugin/.gitignore @@ -0,0 +1,33 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +build/ diff --git a/local_plugins/native_plugin/.metadata b/local_plugins/native_plugin/.metadata new file mode 100644 index 000000000..a5700b901 --- /dev/null +++ b/local_plugins/native_plugin/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + - platform: android + create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + - platform: ios + create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/local_plugins/native_plugin/CHANGELOG.md b/local_plugins/native_plugin/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/local_plugins/native_plugin/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/local_plugins/native_plugin/LICENSE b/local_plugins/native_plugin/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/local_plugins/native_plugin/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/local_plugins/native_plugin/README.md b/local_plugins/native_plugin/README.md new file mode 100644 index 000000000..bc8261f01 --- /dev/null +++ b/local_plugins/native_plugin/README.md @@ -0,0 +1,15 @@ +# native_plugin + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/to/develop-plugins), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/local_plugins/native_plugin/analysis_options.yaml b/local_plugins/native_plugin/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/local_plugins/native_plugin/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/local_plugins/native_plugin/android/.gitignore b/local_plugins/native_plugin/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/local_plugins/native_plugin/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/local_plugins/native_plugin/android/build.gradle b/local_plugins/native_plugin/android/build.gradle new file mode 100644 index 000000000..948c5ef6f --- /dev/null +++ b/local_plugins/native_plugin/android/build.gradle @@ -0,0 +1,52 @@ +group = "com.example.native_plugin" +version = "1.0" + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.7.0") + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: "com.android.library" + +android { + namespace = "com.example.native_plugin" + + compileSdk = 35 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + defaultConfig { + minSdk = 21 + } + + dependencies { + testImplementation("junit:junit:4.13.2") + testImplementation("org.mockito:mockito-core:5.0.0") + } + + testOptions { + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/local_plugins/native_plugin/android/settings.gradle b/local_plugins/native_plugin/android/settings.gradle new file mode 100644 index 000000000..9f756a98c --- /dev/null +++ b/local_plugins/native_plugin/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'native_plugin' diff --git a/local_plugins/native_plugin/android/src/main/AndroidManifest.xml b/local_plugins/native_plugin/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..5153b5dc6 --- /dev/null +++ b/local_plugins/native_plugin/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java b/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java new file mode 100644 index 000000000..265796e9d --- /dev/null +++ b/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java @@ -0,0 +1,38 @@ +package com.example.native_plugin; + +import androidx.annotation.NonNull; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; + +/** NativePlugin */ +public class NativePlugin implements FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private MethodChannel channel; + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { + channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "native_plugin"); + channel.setMethodCallHandler(this); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + if (call.method.equals("getPlatformVersion")) { + result.success("Android " + android.os.Build.VERSION.RELEASE); + } else { + result.notImplemented(); + } + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + channel.setMethodCallHandler(null); + } +} diff --git a/local_plugins/native_plugin/android/src/test/java/com/example/native_plugin/NativePluginTest.java b/local_plugins/native_plugin/android/src/test/java/com/example/native_plugin/NativePluginTest.java new file mode 100644 index 000000000..8034030e9 --- /dev/null +++ b/local_plugins/native_plugin/android/src/test/java/com/example/native_plugin/NativePluginTest.java @@ -0,0 +1,29 @@ +package com.example.native_plugin; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import org.junit.Test; + +/** + * This demonstrates a simple unit test of the Java portion of this plugin's implementation. + * + * Once you have built the plugin's example app, you can run these tests from the command + * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or + * you can run them directly from IDEs that support JUnit such as Android Studio. + */ + +public class NativePluginTest { + @Test + public void onMethodCall_getPlatformVersion_returnsExpectedValue() { + NativePlugin plugin = new NativePlugin(); + + final MethodCall call = new MethodCall("getPlatformVersion", null); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + plugin.onMethodCall(call, mockResult); + + verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE); + } +} diff --git a/local_plugins/native_plugin/ios/.gitignore b/local_plugins/native_plugin/ios/.gitignore new file mode 100644 index 000000000..034771fc9 --- /dev/null +++ b/local_plugins/native_plugin/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh diff --git a/local_plugins/native_plugin/ios/Assets/.gitkeep b/local_plugins/native_plugin/ios/Assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/local_plugins/native_plugin/ios/Classes/FLNativeView.h b/local_plugins/native_plugin/ios/Classes/FLNativeView.h new file mode 100644 index 000000000..4c49e00f4 --- /dev/null +++ b/local_plugins/native_plugin/ios/Classes/FLNativeView.h @@ -0,0 +1,15 @@ +#import + +@interface FLNativeViewFactory : NSObject +- (instancetype)initWithMessenger:(NSObject *)messenger; +@end + +@interface FLNativeView : NSObject + +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject *)messenger; + +- (UIView *)view; +@end diff --git a/local_plugins/native_plugin/ios/Classes/FLNativeView.m b/local_plugins/native_plugin/ios/Classes/FLNativeView.m new file mode 100644 index 000000000..848785a6b --- /dev/null +++ b/local_plugins/native_plugin/ios/Classes/FLNativeView.m @@ -0,0 +1,83 @@ +#import "FLNativeView.h" +#import "PlayerView.h" + +@implementation FLNativeViewFactory { + NSObject *_messenger; +} + +- (instancetype)initWithMessenger: + (NSObject *)messenger { + self = [super init]; + if (self) { + _messenger = messenger; + } + return self; +} + +- (NSObject *)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + return [[FLNativeView alloc] initWithFrame:frame + viewIdentifier:viewId + arguments:args + binaryMessenger:_messenger]; +} + +/// Implementing this method is only necessary when the `arguments` in +/// `createWithFrame` is not `nil`. +- (NSObject *)createArgsCodec { + return [FlutterStandardMessageCodec sharedInstance]; +} + +@end + +@implementation FLNativeView { + PlayerView *_view; + FlutterMethodChannel *_methodChannel; +} + +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject *)messenger { + if (self = [super init]) { + _view = [[PlayerView alloc] init]; + _methodChannel = [FlutterMethodChannel + methodChannelWithName: + [NSString + stringWithFormat:@"native_plugin/native_view_%lld", viewId] + binaryMessenger:messenger]; + __weak typeof(self) weakSelf = self; + [_methodChannel setMethodCallHandler:^(FlutterMethodCall *_Nonnull call, + FlutterResult _Nonnull result) { + typeof(self) strongSelf = weakSelf; + if (strongSelf != nil) { + [strongSelf onMethodCall:call result:result]; + } + }]; + } + return self; +} + +- (UIView *)view { + return _view; +} + +- (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if ([@"getInternalView" isEqualToString:call.method]) { + result(@((uint64_t)(_view))); + } else if ([@"updateContent" isEqualToString:call.method]) { + NSDictionary *data = call.arguments; + NSString *lang = data[@"lang"]; + NSString *cn = data[@"cn"]; + NSString *en = data[@"en"]; + dispatch_async(dispatch_get_main_queue(), ^{ + [_view updateWithLang:lang cn:cn en:en]; + }); + result(nil); + } else { + result(@(0)); + } +} + +@end diff --git a/local_plugins/native_plugin/ios/Classes/NativePlugin.h b/local_plugins/native_plugin/ios/Classes/NativePlugin.h new file mode 100644 index 000000000..2d0b3293e --- /dev/null +++ b/local_plugins/native_plugin/ios/Classes/NativePlugin.h @@ -0,0 +1,4 @@ +#import + +@interface NativePlugin : NSObject +@end diff --git a/local_plugins/native_plugin/ios/Classes/NativePlugin.m b/local_plugins/native_plugin/ios/Classes/NativePlugin.m new file mode 100644 index 000000000..bee63b337 --- /dev/null +++ b/local_plugins/native_plugin/ios/Classes/NativePlugin.m @@ -0,0 +1,114 @@ +#import "NativePlugin.h" +#import "FLNativeView.h" +#import "PlayerView.h" + +#import + +@interface NativePlugin () + +@property(nonatomic, strong) NSMutableArray *_Nonnull pipContentViewArray; + +@end + +@implementation NativePlugin + ++ (void)updateAudioSession { + AVAudioSession *audioSession = [AVAudioSession sharedInstance]; + NSError *categoryError = nil; + if (@available(iOS 14.5, *)) { + [audioSession + setCategory:AVAudioSessionCategoryPlayback + mode:AVAudioSessionModeMoviePlayback + options: + AVAudioSessionCategoryOptionOverrideMutedMicrophoneInterruption + error:&categoryError]; + } else { + // Fallback on earlier versions + } + if (categoryError) { + NSLog(@"Set audio session category error: %@", + categoryError.localizedDescription); + } + NSError *activeError = nil; + [audioSession setActive:YES error:&activeError]; + if (activeError) { + NSLog(@"Set audio session active error: %@", + activeError.localizedDescription); + } +} + ++ (void)registerWithRegistrar:(NSObject *)registrar { + // must call this, otherwise pip will not possible + // https://github.com/jazzychad/PiPBugDemo + [NativePlugin updateAudioSession]; + + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:@"native_plugin" + binaryMessenger:[registrar messenger]]; + NativePlugin *instance = [[NativePlugin alloc] init]; + + FLNativeViewFactory *factory = + [[FLNativeViewFactory alloc] initWithMessenger:registrar.messenger]; + [registrar registerViewFactory:factory withId:@"native_view"]; + + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _pipContentViewArray = [NSMutableArray array]; + } + return self; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call + result:(FlutterResult)result { + if ([@"getPlatformVersion" isEqualToString:call.method]) { + result([@"iOS " + stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); + } else if ([@"createPipContentView" isEqualToString:call.method]) { + PlayerView *playerView = [[PlayerView alloc] init]; + + // should control the play state by event of pip, optimize it later + [playerView play]; + + [self.pipContentViewArray addObject:playerView]; + + result(@((uint64_t)playerView)); + } else if ([@"disposePipContentView" isEqualToString:call.method]) { + uint64_t viewId = [call.arguments unsignedLongLongValue]; + for (PlayerView *playerView in self.pipContentViewArray) { + if ((uint64_t)playerView == viewId) { + [playerView pause]; + [self.pipContentViewArray removeObject:playerView]; + break; + } + } + } else if ([@"updatePipContentView" isEqualToString:call.method]) { + NSDictionary *data = call.arguments; + NSString *lang = data[@"lang"]; + NSString *cn = data[@"cn"]; + NSString *en = data[@"en"]; + NSNumber *viewIdNum = data[@"viewId"]; + dispatch_async(dispatch_get_main_queue(), ^{ + if (viewIdNum != nil) { + uint64_t targetId = viewIdNum.unsignedLongLongValue; + for (PlayerView *playerView in self.pipContentViewArray) { + if ((uint64_t)playerView == targetId) { + [playerView updateWithLang:lang cn:cn en:en]; + break; + } + } + } else { + PlayerView *playerView = [self.pipContentViewArray lastObject]; + [playerView updateWithLang:lang cn:cn en:en]; + } + }); + result(nil); + } else { + result(FlutterMethodNotImplemented); + } +} + +@end diff --git a/local_plugins/native_plugin/ios/Classes/PlayerView.h b/local_plugins/native_plugin/ios/Classes/PlayerView.h new file mode 100644 index 000000000..50044ae41 --- /dev/null +++ b/local_plugins/native_plugin/ios/Classes/PlayerView.h @@ -0,0 +1,15 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PlayerView : UIView + +- (void)play; + +- (void)pause; + +- (void)updateWithLang:(NSString *)lang cn:(NSString *)cn en:(NSString *)en; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/local_plugins/native_plugin/ios/Classes/PlayerView.m b/local_plugins/native_plugin/ios/Classes/PlayerView.m new file mode 100644 index 000000000..1272aef4c --- /dev/null +++ b/local_plugins/native_plugin/ios/Classes/PlayerView.m @@ -0,0 +1,100 @@ +#import "PlayerView.h" + +@interface PlayerView () + +@property(nonatomic, strong) UILabel *titleLabel; +@property(nonatomic, strong) UILabel *langLabel; +@property(nonatomic, strong) UILabel *cnLabel; +@property(nonatomic, strong) UILabel *enLabel; +@property(nonatomic, strong) UIStackView *stackView; + +@end + +@implementation PlayerView + +- (instancetype)init { + if (self = [super init]) { + self.backgroundColor = [UIColor whiteColor]; + + UILabel *title = [[UILabel alloc] init]; + title.text = @"实时翻译"; + title.font = [UIFont boldSystemFontOfSize:22]; + title.textColor = [UIColor blackColor]; + title.textAlignment = NSTextAlignmentCenter; + title.numberOfLines = 1; + self.titleLabel = title; + + UILabel *lang = [[UILabel alloc] init]; + lang.text = @"中文(简体) -> 英语"; + lang.font = [UIFont systemFontOfSize:14]; + lang.textColor = [UIColor darkGrayColor]; + lang.textAlignment = NSTextAlignmentLeft; + lang.numberOfLines = 1; + self.langLabel = lang; + + UILabel *cn = [[UILabel alloc] init]; + cn.font = [UIFont systemFontOfSize:18]; + cn.textColor = [UIColor blackColor]; + cn.textAlignment = NSTextAlignmentLeft; + cn.numberOfLines = 0; + self.cnLabel = cn; + + UILabel *en = [[UILabel alloc] init]; + en.font = [UIFont systemFontOfSize:18]; + en.textColor = [UIColor colorWithRed:0.24 green:0.54 blue:0.90 alpha:1.0]; + en.textAlignment = NSTextAlignmentLeft; + en.numberOfLines = 0; + self.enLabel = en; + + NSMutableParagraphStyle *para = [[NSMutableParagraphStyle alloc] init]; + para.lineSpacing = 4; + + self.cnLabel.attributedText = [[NSAttributedString alloc] initWithString:@"正在识别..." attributes:@{NSParagraphStyleAttributeName: para}]; + + self.enLabel.attributedText = [[NSAttributedString alloc] initWithString:@"翻译中..." attributes:@{NSParagraphStyleAttributeName: para}]; + + UIStackView *stack = [[UIStackView alloc] initWithArrangedSubviews:@[self.titleLabel, self.langLabel, self.cnLabel, self.enLabel]]; + stack.axis = UILayoutConstraintAxisVertical; + stack.spacing = 12; + stack.alignment = UIStackViewAlignmentFill; + stack.distribution = UIStackViewDistributionFill; + stack.translatesAutoresizingMaskIntoConstraints = NO; + self.stackView = stack; + + self.layoutMargins = UIEdgeInsetsMake(16, 16, 16, 16); + [self addSubview:self.stackView]; + + UILayoutGuide *guide = self.layoutMarginsGuide; + [NSLayoutConstraint activateConstraints:@[ + [self.stackView.topAnchor constraintEqualToAnchor:guide.topAnchor], + [self.stackView.leadingAnchor constraintEqualToAnchor:guide.leadingAnchor], + [self.stackView.trailingAnchor constraintEqualToAnchor:guide.trailingAnchor], + [self.stackView.bottomAnchor constraintLessThanOrEqualToAnchor:guide.bottomAnchor] + ]]; + } + return self; +} + +- (void)play { + +} + +- (void)pause { + +} + +- (void)updateWithLang:(NSString *)lang cn:(NSString *)cn en:(NSString *)en { + if (lang != nil) { + self.langLabel.text = lang; + } + NSMutableParagraphStyle *para = [[NSMutableParagraphStyle alloc] init]; + para.lineSpacing = 4; + if (cn != nil) { + self.cnLabel.attributedText = [[NSAttributedString alloc] initWithString:cn attributes:@{NSParagraphStyleAttributeName: para}]; + } + if (en != nil) { + self.enLabel.attributedText = [[NSAttributedString alloc] initWithString:en attributes:@{NSParagraphStyleAttributeName: para}]; + } +} + +@end diff --git a/local_plugins/native_plugin/ios/Resources/PrivacyInfo.xcprivacy b/local_plugins/native_plugin/ios/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..a34b7e2e6 --- /dev/null +++ b/local_plugins/native_plugin/ios/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/local_plugins/native_plugin/ios/native_plugin.podspec b/local_plugins/native_plugin/ios/native_plugin.podspec new file mode 100644 index 000000000..c84e9ffba --- /dev/null +++ b/local_plugins/native_plugin/ios/native_plugin.podspec @@ -0,0 +1,29 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint native_plugin.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'native_plugin' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.platform = :ios, '12.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + + # If your plugin requires a privacy manifest, for example if it uses any + # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your + # plugin's privacy impact, and then uncomment this line. For more information, + # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files + # s.resource_bundles = {'native_plugin_privacy' => ['Resources/PrivacyInfo.xcprivacy']} +end diff --git a/local_plugins/native_plugin/lib/native_plugin.dart b/local_plugins/native_plugin/lib/native_plugin.dart new file mode 100644 index 000000000..c54721f1f --- /dev/null +++ b/local_plugins/native_plugin/lib/native_plugin.dart @@ -0,0 +1,19 @@ +import 'native_plugin_platform_interface.dart'; + +class NativePlugin { + Future getPlatformVersion() { + return NativePluginPlatform.instance.getPlatformVersion(); + } + + Future createPipContentView() { + return NativePluginPlatform.instance.createPipContentView(); + } + + Future updatePipContentView(Map data) { + return NativePluginPlatform.instance.updatePipContentView(data); + } + + Future disposePipContentView(int viewId) { + return NativePluginPlatform.instance.disposePipContentView(viewId); + } +} diff --git a/local_plugins/native_plugin/lib/native_plugin_method_channel.dart b/local_plugins/native_plugin/lib/native_plugin_method_channel.dart new file mode 100644 index 000000000..ec8e18ac5 --- /dev/null +++ b/local_plugins/native_plugin/lib/native_plugin_method_channel.dart @@ -0,0 +1,37 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'native_plugin_platform_interface.dart'; + +/// An implementation of [NativePluginPlatform] that uses method channels. +class MethodChannelNativePlugin extends NativePluginPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('native_plugin'); + + @override + Future getPlatformVersion() async { + final version = await methodChannel.invokeMethod( + 'getPlatformVersion', + ); + return version; + } + + @override + Future createPipContentView() async { + final viewId = await methodChannel.invokeMethod( + 'createPipContentView', + ); + return viewId ?? 0; + } + + @override + Future updatePipContentView(Map data) async { + await methodChannel.invokeMethod('updatePipContentView', data); + } + + @override + Future disposePipContentView(int viewId) async { + await methodChannel.invokeMethod('disposePipContentView', viewId); + } +} diff --git a/local_plugins/native_plugin/lib/native_plugin_platform_interface.dart b/local_plugins/native_plugin/lib/native_plugin_platform_interface.dart new file mode 100644 index 000000000..c26495c2f --- /dev/null +++ b/local_plugins/native_plugin/lib/native_plugin_platform_interface.dart @@ -0,0 +1,47 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'native_plugin_method_channel.dart'; + +abstract class NativePluginPlatform extends PlatformInterface { + /// Constructs a NativePluginPlatform. + NativePluginPlatform() : super(token: _token); + + static final Object _token = Object(); + + static NativePluginPlatform _instance = MethodChannelNativePlugin(); + + /// The default instance of [NativePluginPlatform] to use. + /// + /// Defaults to [MethodChannelNativePlugin]. + static NativePluginPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [NativePluginPlatform] when + /// they register themselves. + static set instance(NativePluginPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } + + Future createPipContentView() { + throw UnimplementedError( + 'createPipContentView() has not been implemented.', + ); + } + + Future updatePipContentView(Map data) { + throw UnimplementedError( + 'updatePipContentView() has not been implemented.', + ); + } + + Future disposePipContentView(int viewId) { + throw UnimplementedError( + 'disposePipContentView() has not been implemented.', + ); + } +} diff --git a/local_plugins/native_plugin/pubspec.yaml b/local_plugins/native_plugin/pubspec.yaml new file mode 100644 index 000000000..a284bfce8 --- /dev/null +++ b/local_plugins/native_plugin/pubspec.yaml @@ -0,0 +1,72 @@ +name: native_plugin +description: "A new Flutter plugin project." +version: 0.0.1 +homepage: + +environment: + sdk: ^3.7.0 + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.example.native_plugin + pluginClass: NativePlugin + ios: + pluginClass: NativePlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/local_plugins/open_ai_service/android/AndroidManifest.xml b/local_plugins/open_ai_service/android/AndroidManifest.xml index 1d834e578..2b2696537 100644 --- a/local_plugins/open_ai_service/android/AndroidManifest.xml +++ b/local_plugins/open_ai_service/android/AndroidManifest.xml @@ -4,9 +4,7 @@ - - - \ No newline at end of file + diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt index 3117bffa6..845b60d23 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt @@ -13,7 +13,6 @@ import android.provider.AlarmClock import android.provider.CalendarContract import android.provider.MediaStore import android.provider.Settings -import android.telephony.SmsManager import android.util.Log import org.json.JSONObject import kotlinx.coroutines.suspendCancellableCoroutine @@ -761,29 +760,31 @@ class SystemFunctionHandler(private val context: Context) { Log.d(TAG, "发送短信给 $contact: $message") try { - // 检查发送短信权限 - if (!checkPermissionAndGuide(Manifest.permission.SEND_SMS, "发送短信", "发送短信")) { + val phoneNumber = getPhoneNumberForContact(contact) + if (phoneNumber.isEmpty()) { return Result( success = false, - message = "需要短信权限才能发送短信,请在权限管理中开启短信权限" + message = "未找到联系人信息: $contact" ) } - val phoneNumber = getPhoneNumberForContact(contact) - if (phoneNumber.isEmpty()) { + val intent = Intent(Intent.ACTION_SENDTO).apply { + data = Uri.parse("smsto:$phoneNumber") + putExtra("sms_body", message) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent) + } else { return Result( success = false, - message = "未找到联系人信息: $contact" + message = "未找到可用的短信应用" ) } - // 使用SmsManager发送短信 - val smsManager = SmsManager.getDefault() - smsManager.sendTextMessage(phoneNumber, null, message, null, null) - return Result( success = true, - message = "已向${contact}发送短信: $message" + message = "已打开短信应用,请确认发送" ) } catch (e: Exception) { Log.e(TAG, "发送短信失败: ${e.message}", e) @@ -1350,4 +1351,4 @@ class SystemFunctionHandler(private val context: Context) { val success: Boolean, val message: String ) -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index c2cbdf8c3..6a6fe9300 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: voitrans description: "Voitrans - AI Voice Assistant." publish_to: "none" -version: 1.0.18+54 +version: 1.0.20+67 environment: sdk: ">=3.3.0 <4.0.0" @@ -15,10 +15,12 @@ dependencies: flutter_shaders: ^0.0.6 get: ^4.7.2 + in_app_purchase: ^3.2.3 json_annotation: ^4.9.0 get_storage: ^2.1.1 http: ^1.3.0 flutter_screenutil: ^5.9.0 + flutter_staggered_grid_view: ^0.7.0 path_provider: ^2.1.5 audio_session: ^0.1.18 flutter_markdown: ^0.7.7+1 @@ -64,6 +66,8 @@ dependencies: fluwx: ^5.7.1 tobias: ^5.2.0 # 支付宝SDK url_launcher: ^6.3.1 + in_app_purchase_android: ^0.4.0+5 + app_links: firebase_core: ^4.0.0 firebase_auth: ^6.0.1 google_sign_in: ^6.2.2 @@ -76,6 +80,7 @@ dependencies: dio: ^5.8.0+1 sqflite: ^2.4.2 path: ^1.9.1 + pip: ^0.0.3 connectivity_plus: ^6.1.5 # 网络连接检测依赖 speech: path: local_plugins/speech @@ -111,9 +116,10 @@ dependencies: path: local_plugins/user_portrait qq_music: path: local_plugins/qq_music + native_plugin: + path: local_plugins/native_plugin test: path: local_plugins/test - in_app_purchase: ^3.2.3 # 应用内购买插件 dev_dependencies: flutter_test: @@ -139,8 +145,8 @@ flutter: - assets/fonts/NotoSansSC-Regular.ttf - assets/lottie/ - assets/js/ - - assets/images/headphone_new4.png - - assets/images/headphone_new3.png + - assets/images/defaultPicture.png + - assets/images/defaultPicture.jpg - assets/images/qqmusic.png - assets/images/music.png - assets/weather/ diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index fae16f5da..227ab6e38 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -19,6 +20,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 44fbd12be..17287648d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links connectivity_plus file_selector_windows firebase_auth