From 9abed0008bf9997c1792fbfff429197f778abb05 Mon Sep 17 00:00:00 2001 From: Rodger-Wang <1367893453@qq.com> Date: Sun, 6 Sep 2026 01:08:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AD=97=E5=B9=95=E5=B0=8F=E7=AA=97=EF=BC=9A?= =?UTF-8?q?=E5=8A=A0=E6=9A=82=E5=81=9C/=E7=BB=A7=E7=BB=AD=E3=80=81?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=E5=BF=AB=E8=BF=9B=E5=BF=AB=E9=80=80=E3=80=81?= =?UTF-8?q?=E5=A4=A7=E5=B0=8F=E4=B8=8E=E9=80=8F=E6=98=8E=E5=BA=A6=E5=8F=AF?= =?UTF-8?q?=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 暂停/继续:实现 AVPictureInPictureSampleBufferPlaybackDelegate 的 setPlaying / isPlaybackPaused。语义定成「冻结当前这条字幕」而不是 「暂停翻译」——用户在别的 App 里看视频、想把一句话定住多看两眼,这是小窗 那颗按钮唯一说得通的含义,而且完全可逆、不影响正在跑的识别与翻译。 冻结期间新文案只暂存,恢复时补上最新一条。 ⚠️ setPlaying 里必须调 invalidatePlaybackState(),否则按钮图标不跟着变, 点了「暂停」它还画着暂停图标,看着像没生效。 - 快进快退:requiresLinearPlayback = true 就是这个开关(原本已设,这次补了 注释说明它的作用,免得以后被当成无关配置删掉)。 - 大小 / 透明度:翻译页新增一颗 tune 按钮,弹出设置面板,两个滑块, GetStorage 持久化。新增 3 个 i18n key(pipSettings/pipSize/pipOpacity), 41 个语言文件全部补齐,主要语种给了本地化文案、其余回退英文。 三条平台限制已写进注释与面板说明,避免以后重复尝试: - 小窗**实际大小由用户捏合手势决定**,App 无权直接设定。「大小」滑块改的是 画布宽高比,系统按新形状重新布局,观感上是变宽/变窄。 - 透明度**做不到透出下面的 App**。iOS 的 PiP 窗口本身不透明,系统未开放接口。 滑块只改卡片底色深浅。要少挡内容只能把小窗调小。 - 播放/暂停按钮的**尺寸不可控**,那是系统绘制的 chrome,随小窗一起缩放—— 所以「按钮小一点」等价于「把小窗调小」。pip 那个 pub 包用私有 API _setControlsStyle 去动控件,上架有被拒风险,不采用。 Co-Authored-By: Claude Opus 5 --- .../lib/core/translations/language/ar_sa.dart | 3 + .../lib/core/translations/language/bg_bg.dart | 3 + .../lib/core/translations/language/cs_cz.dart | 3 + .../lib/core/translations/language/da_dk.dart | 3 + .../lib/core/translations/language/de_de.dart | 3 + .../lib/core/translations/language/el_gr.dart | 3 + .../lib/core/translations/language/en_us.dart | 3 + .../lib/core/translations/language/es_es.dart | 3 + .../lib/core/translations/language/et_ee.dart | 3 + .../lib/core/translations/language/fa_ir.dart | 3 + .../lib/core/translations/language/fi_fi.dart | 3 + .../lib/core/translations/language/fr_fr.dart | 3 + .../lib/core/translations/language/hi_in.dart | 3 + .../lib/core/translations/language/hr_hr.dart | 3 + .../lib/core/translations/language/hu_hu.dart | 3 + .../lib/core/translations/language/id_id.dart | 3 + .../lib/core/translations/language/is_is.dart | 3 + .../lib/core/translations/language/it_it.dart | 3 + .../lib/core/translations/language/ja_jp.dart | 3 + .../lib/core/translations/language/ko_kr.dart | 3 + .../lib/core/translations/language/lt_lt.dart | 3 + .../lib/core/translations/language/lv_lv.dart | 3 + .../lib/core/translations/language/ms_my.dart | 3 + .../lib/core/translations/language/nb_no.dart | 3 + .../lib/core/translations/language/nl_nl.dart | 3 + .../lib/core/translations/language/pl_pl.dart | 3 + .../lib/core/translations/language/pt_pt.dart | 3 + .../lib/core/translations/language/ro_ro.dart | 3 + .../lib/core/translations/language/ru_ru.dart | 3 + .../lib/core/translations/language/sk_sk.dart | 3 + .../lib/core/translations/language/sl_si.dart | 3 + .../lib/core/translations/language/sr_rs.dart | 3 + .../lib/core/translations/language/sv_se.dart | 3 + .../lib/core/translations/language/th_th.dart | 3 + .../lib/core/translations/language/tl_ph.dart | 3 + .../lib/core/translations/language/tr_tr.dart | 3 + .../lib/core/translations/language/uk_ua.dart | 3 + .../lib/core/translations/language/vi_vn.dart | 3 + .../lib/core/translations/language/zh_cn.dart | 3 + .../lib/core/translations/language/zh_hk.dart | 3 + .../lib/core/translations/language/zh_tw.dart | 3 + .../controllers/translation_controller.dart | 13 +++ .../translation/views/translation_view.dart | 106 ++++++++++++++++++ .../ios/Classes/SubtitlePipController.swift | 51 ++++++++- 44 files changed, 290 insertions(+), 3 deletions(-) diff --git a/apps/client/lib/core/translations/language/ar_sa.dart b/apps/client/lib/core/translations/language/ar_sa.dart index c007d1d9..5b5b36d9 100644 --- a/apps/client/lib/core/translations/language/ar_sa.dart +++ b/apps/client/lib/core/translations/language/ar_sa.dart @@ -1078,6 +1078,9 @@ const Map arSA = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'تعطيل النافذة العائمة', + 'pipSettings': 'نافذة الترجمة', + 'pipSize': 'الحجم', + 'pipOpacity': 'العتامة', 'enableFloatingWindow': 'تمكين النافذة العائمة', 'permissionTip': 'تنبيه الأذونات', 'micPermissionRequiredForRecording': 'إذن الميكروفون مطلوب للتسجيل', diff --git a/apps/client/lib/core/translations/language/bg_bg.dart b/apps/client/lib/core/translations/language/bg_bg.dart index d9bdc523..388cf5c9 100644 --- a/apps/client/lib/core/translations/language/bg_bg.dart +++ b/apps/client/lib/core/translations/language/bg_bg.dart @@ -1104,6 +1104,9 @@ const Map bgBG = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Изключи плаващ прозорец', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Включи плаващ прозорец', 'permissionTip': 'Съвет за разрешение', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/cs_cz.dart b/apps/client/lib/core/translations/language/cs_cz.dart index ef04f08d..f8a864f2 100644 --- a/apps/client/lib/core/translations/language/cs_cz.dart +++ b/apps/client/lib/core/translations/language/cs_cz.dart @@ -1088,6 +1088,9 @@ const Map csCZ = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Zakázat plovoucí okno', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Povolit plovoucí okno', 'permissionTip': 'Upozornění na oprávnění', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/da_dk.dart b/apps/client/lib/core/translations/language/da_dk.dart index f9c93e49..39fd2c84 100644 --- a/apps/client/lib/core/translations/language/da_dk.dart +++ b/apps/client/lib/core/translations/language/da_dk.dart @@ -1094,6 +1094,9 @@ const Map daDK = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Deaktivér flydende vindue', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Aktivér flydende vindue', 'permissionTip': 'Tilladelsestip', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/de_de.dart b/apps/client/lib/core/translations/language/de_de.dart index 0a6fdf93..4b079dc6 100644 --- a/apps/client/lib/core/translations/language/de_de.dart +++ b/apps/client/lib/core/translations/language/de_de.dart @@ -1113,6 +1113,9 @@ const Map deDE = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Schwebendes Fenster deaktivieren', + 'pipSettings': 'Untertitelfenster', + 'pipSize': 'Größe', + 'pipOpacity': 'Deckkraft', 'enableFloatingWindow': 'Schwebendes Fenster aktivieren', 'permissionTip': 'Berechtigungshinweis', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/el_gr.dart b/apps/client/lib/core/translations/language/el_gr.dart index 9d7337d7..2a2aa703 100644 --- a/apps/client/lib/core/translations/language/el_gr.dart +++ b/apps/client/lib/core/translations/language/el_gr.dart @@ -1117,6 +1117,9 @@ const Map elGR = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Απενεργοποίηση αιωρούμενου παραθύρου', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Ενεργοποίηση αιωρούμενου παραθύρου', 'permissionTip': 'Υπόδειξη δικαιώματος', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/en_us.dart b/apps/client/lib/core/translations/language/en_us.dart index 46faeff7..b7c0beaf 100644 --- a/apps/client/lib/core/translations/language/en_us.dart +++ b/apps/client/lib/core/translations/language/en_us.dart @@ -1122,6 +1122,9 @@ const Map enUS = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Disable Floating Window', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Enable Floating Window', 'permissionTip': 'Permission Tip', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/es_es.dart b/apps/client/lib/core/translations/language/es_es.dart index aeb4ea61..9787193f 100644 --- a/apps/client/lib/core/translations/language/es_es.dart +++ b/apps/client/lib/core/translations/language/es_es.dart @@ -1112,6 +1112,9 @@ const Map esES = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Desactivar ventana flotante', + 'pipSettings': 'Ventana de subtítulos', + 'pipSize': 'Tamaño', + 'pipOpacity': 'Opacidad', 'enableFloatingWindow': 'Activar ventana flotante', 'permissionTip': 'Aviso de permiso', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/et_ee.dart b/apps/client/lib/core/translations/language/et_ee.dart index da1efd18..e6a34dc2 100644 --- a/apps/client/lib/core/translations/language/et_ee.dart +++ b/apps/client/lib/core/translations/language/et_ee.dart @@ -1094,6 +1094,9 @@ const Map etEE = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Keela ujuvaken', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Luba ujuvaken', 'permissionTip': 'Lubade teade', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/fa_ir.dart b/apps/client/lib/core/translations/language/fa_ir.dart index e24b17b6..8b7254e1 100644 --- a/apps/client/lib/core/translations/language/fa_ir.dart +++ b/apps/client/lib/core/translations/language/fa_ir.dart @@ -1081,6 +1081,9 @@ const Map faIR = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'غیرفعال کردن پنجره شناور', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'فعال کردن پنجره شناور', 'permissionTip': 'نکته مجوز', 'micPermissionRequiredForRecording': 'مجوز میکروفون برای ضبط الزامی است', diff --git a/apps/client/lib/core/translations/language/fi_fi.dart b/apps/client/lib/core/translations/language/fi_fi.dart index 0d33b0ec..116c6ba2 100644 --- a/apps/client/lib/core/translations/language/fi_fi.dart +++ b/apps/client/lib/core/translations/language/fi_fi.dart @@ -1094,6 +1094,9 @@ const Map fiFI = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Poista kelluva ikkuna käytöstä', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Ota kelluva ikkuna käyttöön', 'permissionTip': 'Lupavihje', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/fr_fr.dart b/apps/client/lib/core/translations/language/fr_fr.dart index c591e412..3bae2a87 100644 --- a/apps/client/lib/core/translations/language/fr_fr.dart +++ b/apps/client/lib/core/translations/language/fr_fr.dart @@ -1123,6 +1123,9 @@ const Map frFR = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Desactiver la fenetre flottante', + 'pipSettings': 'Fenêtre de sous-titres', + 'pipSize': 'Taille', + 'pipOpacity': 'Opacité', 'enableFloatingWindow': 'Activer la fenetre flottante', 'permissionTip': 'Conseil d autorisation', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/hi_in.dart b/apps/client/lib/core/translations/language/hi_in.dart index 6f12935a..7573f1c9 100644 --- a/apps/client/lib/core/translations/language/hi_in.dart +++ b/apps/client/lib/core/translations/language/hi_in.dart @@ -1093,6 +1093,9 @@ const Map hiIN = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'फ़्लोटिंग विंडो अक्षम करें', + 'pipSettings': 'उपशीर्षक विंडो', + 'pipSize': 'आकार', + 'pipOpacity': 'अपारदर्शिता', 'enableFloatingWindow': 'फ़्लोटिंग विंडो सक्षम करें', 'permissionTip': 'अनुमति सुझाव', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/hr_hr.dart b/apps/client/lib/core/translations/language/hr_hr.dart index 1e798ee1..deaddb0d 100644 --- a/apps/client/lib/core/translations/language/hr_hr.dart +++ b/apps/client/lib/core/translations/language/hr_hr.dart @@ -1102,6 +1102,9 @@ const Map hrHR = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Onemogući plutajući prozor', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Omogući plutajući prozor', 'permissionTip': 'Savjet o dopuštenjima', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/hu_hu.dart b/apps/client/lib/core/translations/language/hu_hu.dart index 6443dc7a..831a13e2 100644 --- a/apps/client/lib/core/translations/language/hu_hu.dart +++ b/apps/client/lib/core/translations/language/hu_hu.dart @@ -1106,6 +1106,9 @@ const Map huHU = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Lebegő ablak letiltása', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Lebegő ablak engedélyezése', 'permissionTip': 'Engedélyek tipp', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/id_id.dart b/apps/client/lib/core/translations/language/id_id.dart index ec7a5b8b..5e453825 100644 --- a/apps/client/lib/core/translations/language/id_id.dart +++ b/apps/client/lib/core/translations/language/id_id.dart @@ -1098,6 +1098,9 @@ const Map idID = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Nonaktifkan Jendela Mengambang', + 'pipSettings': 'Jendela subtitle', + 'pipSize': 'Ukuran', + 'pipOpacity': 'Opasitas', 'enableFloatingWindow': 'Aktifkan Jendela Mengambang', 'permissionTip': 'Tips Izin', 'micPermissionRequiredForRecording': 'Izin mikrofon diperlukan untuk merekam', diff --git a/apps/client/lib/core/translations/language/is_is.dart b/apps/client/lib/core/translations/language/is_is.dart index d8a80cc4..ac9b7842 100644 --- a/apps/client/lib/core/translations/language/is_is.dart +++ b/apps/client/lib/core/translations/language/is_is.dart @@ -1096,6 +1096,9 @@ const Map isIS = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Slökkva á fljótandi glugga', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Virkja fljótandi glugga', 'permissionTip': 'Heimildaábending', 'micPermissionRequiredForRecording': 'Hljóðnemaheimild þarf til upptöku', diff --git a/apps/client/lib/core/translations/language/it_it.dart b/apps/client/lib/core/translations/language/it_it.dart index a5887969..1547c2e6 100644 --- a/apps/client/lib/core/translations/language/it_it.dart +++ b/apps/client/lib/core/translations/language/it_it.dart @@ -1116,6 +1116,9 @@ const Map itIT = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Disattiva finestra mobile', + 'pipSettings': 'Finestra sottotitoli', + 'pipSize': 'Dimensione', + 'pipOpacity': 'Opacità', 'enableFloatingWindow': 'Attiva finestra mobile', 'permissionTip': 'Suggerimento permessi', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/ja_jp.dart b/apps/client/lib/core/translations/language/ja_jp.dart index 374ec0a1..6e995c6c 100644 --- a/apps/client/lib/core/translations/language/ja_jp.dart +++ b/apps/client/lib/core/translations/language/ja_jp.dart @@ -1017,6 +1017,9 @@ const Map jaJP = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'フローティングウィンドウを無効化', + 'pipSettings': '字幕ウィンドウ', + 'pipSize': 'サイズ', + 'pipOpacity': '不透明度', 'enableFloatingWindow': 'フローティングウィンドウを有効化', 'permissionTip': '権限のヒント', 'micPermissionRequiredForRecording': '録音にはマイク権限が必要です', diff --git a/apps/client/lib/core/translations/language/ko_kr.dart b/apps/client/lib/core/translations/language/ko_kr.dart index a6f2de4d..8800cc14 100644 --- a/apps/client/lib/core/translations/language/ko_kr.dart +++ b/apps/client/lib/core/translations/language/ko_kr.dart @@ -1015,6 +1015,9 @@ const Map koKR = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': '플로팅 창 비활성화', + 'pipSettings': '자막 창', + 'pipSize': '크기', + 'pipOpacity': '불투명도', 'enableFloatingWindow': '플로팅 창 활성화', 'permissionTip': '권한 안내', 'micPermissionRequiredForRecording': '녹음에는 마이크 권한이 필요합니다', diff --git a/apps/client/lib/core/translations/language/lt_lt.dart b/apps/client/lib/core/translations/language/lt_lt.dart index e33aecd6..44751817 100644 --- a/apps/client/lib/core/translations/language/lt_lt.dart +++ b/apps/client/lib/core/translations/language/lt_lt.dart @@ -1103,6 +1103,9 @@ const Map ltLT = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Išjungti plaukiojantį langą', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Įjungti plaukiojantį langą', 'permissionTip': 'Leidimų patarimas', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/lv_lv.dart b/apps/client/lib/core/translations/language/lv_lv.dart index 1eea049f..ba7b59b0 100644 --- a/apps/client/lib/core/translations/language/lv_lv.dart +++ b/apps/client/lib/core/translations/language/lv_lv.dart @@ -1107,6 +1107,9 @@ const Map lvLV = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Atspējot peldošo logu', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Iespējot peldošo logu', 'permissionTip': 'Atļauju padoms', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/ms_my.dart b/apps/client/lib/core/translations/language/ms_my.dart index 26bc7292..548d2ca6 100644 --- a/apps/client/lib/core/translations/language/ms_my.dart +++ b/apps/client/lib/core/translations/language/ms_my.dart @@ -1100,6 +1100,9 @@ const Map msMY = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Nyahdayakan Tetingkap Terapung', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Dayakan Tetingkap Terapung', 'permissionTip': 'Petua Kebenaran', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/nb_no.dart b/apps/client/lib/core/translations/language/nb_no.dart index e7b922bf..dd4582fe 100644 --- a/apps/client/lib/core/translations/language/nb_no.dart +++ b/apps/client/lib/core/translations/language/nb_no.dart @@ -1099,6 +1099,9 @@ const Map nbNO = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Deaktiver flytende vindu', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Aktiver flytende vindu', 'permissionTip': 'Tillatelsestips', 'micPermissionRequiredForRecording': 'Mikrofontillatelse kreves for opptak', diff --git a/apps/client/lib/core/translations/language/nl_nl.dart b/apps/client/lib/core/translations/language/nl_nl.dart index 7acba09f..1168c7bd 100644 --- a/apps/client/lib/core/translations/language/nl_nl.dart +++ b/apps/client/lib/core/translations/language/nl_nl.dart @@ -983,6 +983,9 @@ const Map nlNL = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Zwevend venster uitschakelen', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Zwevend venster inschakelen', 'permissionTip': 'Machtigingstip', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/pl_pl.dart b/apps/client/lib/core/translations/language/pl_pl.dart index 84ff763d..100bfbbc 100644 --- a/apps/client/lib/core/translations/language/pl_pl.dart +++ b/apps/client/lib/core/translations/language/pl_pl.dart @@ -1110,6 +1110,9 @@ const Map plPL = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Wyłącz pływające okno', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Włącz pływające okno', 'permissionTip': 'Wskazówka dotycząca uprawnień', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/pt_pt.dart b/apps/client/lib/core/translations/language/pt_pt.dart index 6505dd3c..9b55a5c4 100644 --- a/apps/client/lib/core/translations/language/pt_pt.dart +++ b/apps/client/lib/core/translations/language/pt_pt.dart @@ -1117,6 +1117,9 @@ const Map ptPT = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Desativar janela flutuante', + 'pipSettings': 'Janela de legendas', + 'pipSize': 'Tamanho', + 'pipOpacity': 'Opacidade', 'enableFloatingWindow': 'Ativar janela flutuante', 'permissionTip': 'Dica de permissao', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/ro_ro.dart b/apps/client/lib/core/translations/language/ro_ro.dart index 2c2fbe77..3607830c 100644 --- a/apps/client/lib/core/translations/language/ro_ro.dart +++ b/apps/client/lib/core/translations/language/ro_ro.dart @@ -1108,6 +1108,9 @@ const Map roRO = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Dezactivare fereastra plutitoare', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Activare fereastra plutitoare', 'permissionTip': 'Sfat privind permisiunile', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/ru_ru.dart b/apps/client/lib/core/translations/language/ru_ru.dart index c9361d1d..c99e5aa0 100644 --- a/apps/client/lib/core/translations/language/ru_ru.dart +++ b/apps/client/lib/core/translations/language/ru_ru.dart @@ -1101,6 +1101,9 @@ const Map ruRU = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Отключить плавающее окно', + 'pipSettings': 'Окно субтитров', + 'pipSize': 'Размер', + 'pipOpacity': 'Непрозрачность', 'enableFloatingWindow': 'Включить плавающее окно', 'permissionTip': 'Подсказка о разрешениях', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/sk_sk.dart b/apps/client/lib/core/translations/language/sk_sk.dart index ab8ba4f7..728c75f9 100644 --- a/apps/client/lib/core/translations/language/sk_sk.dart +++ b/apps/client/lib/core/translations/language/sk_sk.dart @@ -1098,6 +1098,9 @@ const Map skSK = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Vypnúť plávajúce okno', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Zapnúť plávajúce okno', 'permissionTip': 'Tip k povoleniam', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/sl_si.dart b/apps/client/lib/core/translations/language/sl_si.dart index a9285f5a..0bfcc84c 100644 --- a/apps/client/lib/core/translations/language/sl_si.dart +++ b/apps/client/lib/core/translations/language/sl_si.dart @@ -1101,6 +1101,9 @@ const Map slSI = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Onemogoči plavajoče okno', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Omogoči plavajoče okno', 'permissionTip': 'Namig za dovoljenja', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/sr_rs.dart b/apps/client/lib/core/translations/language/sr_rs.dart index 349ebb17..379fd856 100644 --- a/apps/client/lib/core/translations/language/sr_rs.dart +++ b/apps/client/lib/core/translations/language/sr_rs.dart @@ -1096,6 +1096,9 @@ const Map srRS = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Onemogući plutajući prozor', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Omogući plutajući prozor', 'permissionTip': 'Savet o dozvoli', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/sv_se.dart b/apps/client/lib/core/translations/language/sv_se.dart index fd2b52cd..c48f4816 100644 --- a/apps/client/lib/core/translations/language/sv_se.dart +++ b/apps/client/lib/core/translations/language/sv_se.dart @@ -1097,6 +1097,9 @@ const Map svSE = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Inaktivera flytande fönster', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Aktivera flytande fönster', 'permissionTip': 'Behörighetstips', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/th_th.dart b/apps/client/lib/core/translations/language/th_th.dart index 0a7bc5c3..14a979e0 100644 --- a/apps/client/lib/core/translations/language/th_th.dart +++ b/apps/client/lib/core/translations/language/th_th.dart @@ -1070,6 +1070,9 @@ const Map thTH = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'ปิดหน้าต่างลอย', + 'pipSettings': 'หน้าต่างคำบรรยาย', + 'pipSize': 'ขนาด', + 'pipOpacity': 'ความทึบ', 'enableFloatingWindow': 'เปิดหน้าต่างลอย', 'permissionTip': 'คำแนะนำสิทธิ์', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/tl_ph.dart b/apps/client/lib/core/translations/language/tl_ph.dart index 7c2b2bcc..968886a7 100644 --- a/apps/client/lib/core/translations/language/tl_ph.dart +++ b/apps/client/lib/core/translations/language/tl_ph.dart @@ -1115,6 +1115,9 @@ const Map tlPH = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'I-disable ang floating window', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'I-enable ang floating window', 'permissionTip': 'Paalaala sa pahintulot', 'micPermissionRequiredForRecording': diff --git a/apps/client/lib/core/translations/language/tr_tr.dart b/apps/client/lib/core/translations/language/tr_tr.dart index 03219662..05e0d3bb 100644 --- a/apps/client/lib/core/translations/language/tr_tr.dart +++ b/apps/client/lib/core/translations/language/tr_tr.dart @@ -1094,6 +1094,9 @@ const Map trTR = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Yüzen Pencereyi Devre Dışı Bırak', + 'pipSettings': 'Altyazı penceresi', + 'pipSize': 'Boyut', + 'pipOpacity': 'Opaklık', 'enableFloatingWindow': 'Yüzen Pencereyi Etkinleştir', 'permissionTip': 'İzin İpucu', 'micPermissionRequiredForRecording': 'Kayıt için mikrofon izni gereklidir', diff --git a/apps/client/lib/core/translations/language/uk_ua.dart b/apps/client/lib/core/translations/language/uk_ua.dart index e3d26777..4243bd76 100644 --- a/apps/client/lib/core/translations/language/uk_ua.dart +++ b/apps/client/lib/core/translations/language/uk_ua.dart @@ -1100,6 +1100,9 @@ const Map ukUA = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Вимкнути плаваюче вікно', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': 'Увімкнути плаваюче вікно', 'permissionTip': 'Підказка щодо дозволів', 'micPermissionRequiredForRecording': 'Для запису потрібен дозвіл на мікрофон', diff --git a/apps/client/lib/core/translations/language/vi_vn.dart b/apps/client/lib/core/translations/language/vi_vn.dart index 5769b131..916b68ab 100644 --- a/apps/client/lib/core/translations/language/vi_vn.dart +++ b/apps/client/lib/core/translations/language/vi_vn.dart @@ -1086,6 +1086,9 @@ const Map viVN = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': 'Tắt cửa sổ nổi', + 'pipSettings': 'Cửa sổ phụ đề', + 'pipSize': 'Kích thước', + 'pipOpacity': 'Độ mờ', 'enableFloatingWindow': 'Bật cửa sổ nổi', 'permissionTip': 'Gợi ý quyền', 'micPermissionRequiredForRecording': 'Cần quyền micrô để ghi âm', diff --git a/apps/client/lib/core/translations/language/zh_cn.dart b/apps/client/lib/core/translations/language/zh_cn.dart index 2fc3c45c..5170de22 100644 --- a/apps/client/lib/core/translations/language/zh_cn.dart +++ b/apps/client/lib/core/translations/language/zh_cn.dart @@ -1030,6 +1030,9 @@ const Map zhCN = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': '禁用悬浮窗', + 'pipSettings': '弹幕设置', + 'pipSize': '大小', + 'pipOpacity': '透明度', 'enableFloatingWindow': '启用悬浮窗', 'permissionTip': '权限提示', 'micPermissionRequiredForRecording': '需要麦克风权限才能使用录音功能', diff --git a/apps/client/lib/core/translations/language/zh_hk.dart b/apps/client/lib/core/translations/language/zh_hk.dart index fa94e118..854bafb2 100644 --- a/apps/client/lib/core/translations/language/zh_hk.dart +++ b/apps/client/lib/core/translations/language/zh_hk.dart @@ -1016,6 +1016,9 @@ const Map zhHK = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': '禁用懸浮窗', + 'pipSettings': 'Subtitle window', + 'pipSize': 'Size', + 'pipOpacity': 'Opacity', 'enableFloatingWindow': '啟用懸浮窗', 'permissionTip': '權限提示', 'micPermissionRequiredForRecording': '需要麥克風權限才能使用錄音功能', diff --git a/apps/client/lib/core/translations/language/zh_tw.dart b/apps/client/lib/core/translations/language/zh_tw.dart index eb8ad42e..8afb49f8 100644 --- a/apps/client/lib/core/translations/language/zh_tw.dart +++ b/apps/client/lib/core/translations/language/zh_tw.dart @@ -1016,6 +1016,9 @@ const Map zhTW = { // 翻译模块 - 悬浮窗与权限 'disableFloatingWindow': '停用懸浮視窗', + 'pipSettings': '彈幕設定', + 'pipSize': '大小', + 'pipOpacity': '透明度', 'enableFloatingWindow': '啟用懸浮視窗', 'permissionTip': '權限提示', 'micPermissionRequiredForRecording': '需要麥克風權限才能使用錄音功能', diff --git a/apps/client/lib/modules/translation/controllers/translation_controller.dart b/apps/client/lib/modules/translation/controllers/translation_controller.dart index 1d7a7b95..6731f7d5 100644 --- a/apps/client/lib/modules/translation/controllers/translation_controller.dart +++ b/apps/client/lib/modules/translation/controllers/translation_controller.dart @@ -266,6 +266,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { // 根本不在 App 的窗口列表里 —— 于是字幕落在 App 自己身上(还盖住整屏), // 小窗里只有一块黑的。现在改成自绘:把字幕画进 CVPixelBuffer 逐帧喂给 // AVSampleBufferDisplayLayer,小窗显示的就是它(见原生 SubtitlePipController)。 + _restorePipStyle(); isFloatingWindowEnabled.value = await SubtitlePip.isActive(); } @@ -310,13 +311,25 @@ class TranslationController extends GetxController with WidgetsBindingObserver { final RxDouble pipOpacity = 0.9.obs; final RxDouble pipWidthScale = 1.0.obs; + static const String _kPipOpacityKey = 'pip_opacity'; + static const String _kPipWidthScaleKey = 'pip_width_scale'; + + void _restorePipStyle() { + final o = _storage.read(_kPipOpacityKey); + final w = _storage.read(_kPipWidthScaleKey); + if (o != null) pipOpacity.value = o.clamp(0.2, 1.0); + if (w != null) pipWidthScale.value = w.clamp(0.5, 2.0); + } + Future setPipOpacity(double v) async { pipOpacity.value = v.clamp(0.2, 1.0); + await _storage.write(_kPipOpacityKey, pipOpacity.value); await SubtitlePip.setStyle(opacity: pipOpacity.value); } Future setPipWidthScale(double v) async { pipWidthScale.value = v.clamp(0.5, 2.0); + await _storage.write(_kPipWidthScaleKey, pipWidthScale.value); await SubtitlePip.setStyle(widthScale: pipWidthScale.value); } diff --git a/apps/client/lib/modules/translation/views/translation_view.dart b/apps/client/lib/modules/translation/views/translation_view.dart index 14a7d3b4..4b12ed40 100644 --- a/apps/client/lib/modules/translation/views/translation_view.dart +++ b/apps/client/lib/modules/translation/views/translation_view.dart @@ -30,6 +30,94 @@ enum _TranslationExitAction { continueInBackground, end, cancel } //主页面 class TranslationView extends GetView { + + /// 字幕小窗(PiP)的样式设置。 + /// + /// ⚠️ 这里只有两项,是因为 iOS 的 PiP 就只开放了这么点: + /// - **小窗实际大小由用户捏合手势决定**,App 无权直接设定。这里的「大小」 + /// 改的是画布宽高比,系统会按新形状重新布局,观感上就是变宽/变窄。 + /// - **透明度做不到透出下面的 App**。iOS 的 PiP 窗口本身不透明,系统没有 + /// 开放任何接口。这个滑块只让卡片底色深浅不同。真要少挡内容就把它调小。 + /// - 小窗上那两颗播放/暂停按钮的**尺寸也不可控**,是系统 chrome, + /// 它随小窗一起缩放——所以「按钮小一点」等价于「把小窗调小」。 + void _showPipSettings(BuildContext context, bool isDarkMode) { + Get.bottomSheet( + Container( + padding: EdgeInsets.fromLTRB(20.w, 16.h, 20.w, 28.h), + decoration: BoxDecoration( + color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('pipSettings'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black87)), + SizedBox(height: 12.h), + _pipSlider( + label: 'pipSize'.tr, + isDarkMode: isDarkMode, + value: () => controller.pipWidthScale.value, + min: 0.5, + max: 2.0, + onChanged: controller.setPipWidthScale, + ), + _pipSlider( + label: 'pipOpacity'.tr, + isDarkMode: isDarkMode, + value: () => controller.pipOpacity.value, + min: 0.2, + max: 1.0, + onChanged: controller.setPipOpacity, + ), + ], + ), + ), + isScrollControlled: true, + ); + } + + Widget _pipSlider({ + required String label, + required bool isDarkMode, + required double Function() value, + required double min, + required double max, + required Future Function(double) onChanged, + }) { + return Obx(() => Row( + children: [ + SizedBox( + width: 72.w, + child: Text(label, + style: TextStyle( + fontSize: 13.sp, + color: isDarkMode ? Colors.white70 : Colors.black54)), + ), + Expanded( + child: Slider( + value: value().clamp(min, max), + min: min, + max: max, + onChanged: (v) => onChanged(v), + ), + ), + SizedBox( + width: 44.w, + child: Text('${(value() * 100).round()}%', + textAlign: TextAlign.end, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white54 : Colors.black45)), + ), + ], + )); + } + const TranslationView({Key? key}) : super(key: key); // 本地交互状态:当前显示操作按钮的消息时间戳(不改动控制器) @@ -252,6 +340,24 @@ class TranslationView extends GetView { } return const SizedBox.shrink(); }), + // 小窗样式设置。单独一颗按钮而不是长按同一颗——长按是隐藏 + // 交互,用户发现不了,而这两项恰恰是需要一边看效果一边调的。 + Obx(() { + if (Platform.isIOS && + (controller.currentMode.value == 'call' || + controller.currentMode.value == 'audioVideo')) { + return IconButton( + icon: Icon( + Icons.tune, + color: isDarkMode ? Colors.white70 : Colors.black54, + size: 20.sp, + ), + onPressed: () => _showPipSettings(context, isDarkMode), + tooltip: 'pipSettings'.tr, + ); + } + return const SizedBox.shrink(); + }), IconButton( icon: Icon( Icons.radio_button_checked, diff --git a/apps/client/local_plugins/floating_ui_plugin/ios/Classes/SubtitlePipController.swift b/apps/client/local_plugins/floating_ui_plugin/ios/Classes/SubtitlePipController.swift index d38f195d..e346e138 100644 --- a/apps/client/local_plugins/floating_ui_plugin/ios/Classes/SubtitlePipController.swift +++ b/apps/client/local_plugins/floating_ui_plugin/ios/Classes/SubtitlePipController.swift @@ -50,6 +50,17 @@ final class SubtitlePipController: NSObject { private(set) var isActive = false + /// 小窗上的暂停按钮。 + /// + /// 语义定成「**冻结当前这条字幕**」而不是「暂停翻译」:用户在别的 App 里 + /// 看视频,想把一句话定住多看两眼,这是小窗那个按钮唯一说得通的含义; + /// 而且它完全可逆,不会影响正在跑的识别与翻译。 + /// 暂停期间新来的文案只暂存,恢复时一次性补上最新的一条。 + private var isPaused = false + private var pendingLang: String? + private var pendingCn: String? + private var pendingEn: String? + private override init() { super.init() card.setFillsBounds(true) @@ -76,7 +87,14 @@ final class SubtitlePipController: NSObject { ) let controller = AVPictureInPictureController(contentSource: source) controller.delegate = self - // 字幕不是可拖动进度的媒体,去掉播放/快进那套控件 + // ⚠️ 这一行就是「不要快进快退按钮」:requiresLinearPlayback 会让系统 + // 隐藏小窗上的前进/后退 15 秒,只留播放/暂停。 + // + // ⚠️ 按钮大小**不可控**。那是系统绘制的小窗 chrome,没有公开接口能改 + // 尺寸;它随小窗一起缩放,所以想让按钮小一点,只能把小窗做小 + //(捏合手势,或调 widthScale 换个更窄的比例)。 + // pip 那个 pub 包用私有 API `_setControlsStyle` 去动控件, + // 上架有被拒风险,这里不采用。 controller.requiresLinearPlayback = true // 用户切走 App 时自动进小窗——这正是本功能的使用场景 controller.canStartPictureInPictureAutomaticallyFromInline = true @@ -108,6 +126,13 @@ final class SubtitlePipController: NSObject { func update(lang: String?, cn: String?, en: String?) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } + if self.isPaused { + // 冻结中:只记下最新的一条,恢复时再上屏 + if lang != nil { self.pendingLang = lang } + if cn != nil { self.pendingCn = cn } + if en != nil { self.pendingEn = en } + return + } self.card.update(withLang: lang, cn: cn, en: en) self.renderAndEnqueue(force: true) } @@ -266,7 +291,27 @@ extension SubtitlePipController: AVPictureInPictureControllerDelegate { @available(iOS 15.0, *) extension SubtitlePipController: AVPictureInPictureSampleBufferPlaybackDelegate { func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, - setPlaying playing: Bool) {} + setPlaying playing: Bool) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.isPaused = !playing + if playing { + // 恢复时把冻结期间攒下的最新一条补上 + if self.pendingLang != nil || self.pendingCn != nil || self.pendingEn != nil { + self.card.update(withLang: self.pendingLang, + cn: self.pendingCn, + en: self.pendingEn) + self.pendingLang = nil + self.pendingCn = nil + self.pendingEn = nil + } + self.renderAndEnqueue(force: true) + } + // ⚠️ 必须通知系统重新问一遍播放状态,否则小窗上那颗按钮的图标 + // 不会跟着变——点了「暂停」它还画着暂停图标,看着像没生效。 + pictureInPictureController.invalidatePlaybackState() + } + } /// 返回无限区间 = 告诉系统这是直播流,小窗上就不会出现进度条和快进按钮。 func pictureInPictureControllerTimeRangeForPlayback( @@ -276,7 +321,7 @@ extension SubtitlePipController: AVPictureInPictureSampleBufferPlaybackDelegate func pictureInPictureControllerIsPlaybackPaused( _ pictureInPictureController: AVPictureInPictureController) -> Bool { - false + isPaused } func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController,