Browse Source

字幕小窗:加暂停/继续、去掉快进快退、大小与透明度可调

- 暂停/继续:实现 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 <noreply@anthropic.com>
main
Rodger-Wang 1 month ago
parent
commit
9abed0008b
  1. 3
      apps/client/lib/core/translations/language/ar_sa.dart
  2. 3
      apps/client/lib/core/translations/language/bg_bg.dart
  3. 3
      apps/client/lib/core/translations/language/cs_cz.dart
  4. 3
      apps/client/lib/core/translations/language/da_dk.dart
  5. 3
      apps/client/lib/core/translations/language/de_de.dart
  6. 3
      apps/client/lib/core/translations/language/el_gr.dart
  7. 3
      apps/client/lib/core/translations/language/en_us.dart
  8. 3
      apps/client/lib/core/translations/language/es_es.dart
  9. 3
      apps/client/lib/core/translations/language/et_ee.dart
  10. 3
      apps/client/lib/core/translations/language/fa_ir.dart
  11. 3
      apps/client/lib/core/translations/language/fi_fi.dart
  12. 3
      apps/client/lib/core/translations/language/fr_fr.dart
  13. 3
      apps/client/lib/core/translations/language/hi_in.dart
  14. 3
      apps/client/lib/core/translations/language/hr_hr.dart
  15. 3
      apps/client/lib/core/translations/language/hu_hu.dart
  16. 3
      apps/client/lib/core/translations/language/id_id.dart
  17. 3
      apps/client/lib/core/translations/language/is_is.dart
  18. 3
      apps/client/lib/core/translations/language/it_it.dart
  19. 3
      apps/client/lib/core/translations/language/ja_jp.dart
  20. 3
      apps/client/lib/core/translations/language/ko_kr.dart
  21. 3
      apps/client/lib/core/translations/language/lt_lt.dart
  22. 3
      apps/client/lib/core/translations/language/lv_lv.dart
  23. 3
      apps/client/lib/core/translations/language/ms_my.dart
  24. 3
      apps/client/lib/core/translations/language/nb_no.dart
  25. 3
      apps/client/lib/core/translations/language/nl_nl.dart
  26. 3
      apps/client/lib/core/translations/language/pl_pl.dart
  27. 3
      apps/client/lib/core/translations/language/pt_pt.dart
  28. 3
      apps/client/lib/core/translations/language/ro_ro.dart
  29. 3
      apps/client/lib/core/translations/language/ru_ru.dart
  30. 3
      apps/client/lib/core/translations/language/sk_sk.dart
  31. 3
      apps/client/lib/core/translations/language/sl_si.dart
  32. 3
      apps/client/lib/core/translations/language/sr_rs.dart
  33. 3
      apps/client/lib/core/translations/language/sv_se.dart
  34. 3
      apps/client/lib/core/translations/language/th_th.dart
  35. 3
      apps/client/lib/core/translations/language/tl_ph.dart
  36. 3
      apps/client/lib/core/translations/language/tr_tr.dart
  37. 3
      apps/client/lib/core/translations/language/uk_ua.dart
  38. 3
      apps/client/lib/core/translations/language/vi_vn.dart
  39. 3
      apps/client/lib/core/translations/language/zh_cn.dart
  40. 3
      apps/client/lib/core/translations/language/zh_hk.dart
  41. 3
      apps/client/lib/core/translations/language/zh_tw.dart
  42. 13
      apps/client/lib/modules/translation/controllers/translation_controller.dart
  43. 106
      apps/client/lib/modules/translation/views/translation_view.dart
  44. 51
      apps/client/local_plugins/floating_ui_plugin/ios/Classes/SubtitlePipController.swift

3
apps/client/lib/core/translations/language/ar_sa.dart

@ -1078,6 +1078,9 @@ const Map<String, String> arSA = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'تعطيل النافذة العائمة',
'pipSettings': 'نافذة الترجمة',
'pipSize': 'الحجم',
'pipOpacity': 'العتامة',
'enableFloatingWindow': 'تمكين النافذة العائمة',
'permissionTip': 'تنبيه الأذونات',
'micPermissionRequiredForRecording': 'إذن الميكروفون مطلوب للتسجيل',

3
apps/client/lib/core/translations/language/bg_bg.dart

@ -1104,6 +1104,9 @@ const Map<String, String> bgBG = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Изключи плаващ прозорец',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': 'Включи плаващ прозорец',
'permissionTip': 'Съвет за разрешение',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/cs_cz.dart

@ -1088,6 +1088,9 @@ const Map<String, String> 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':

3
apps/client/lib/core/translations/language/da_dk.dart

@ -1094,6 +1094,9 @@ const Map<String, String> daDK = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Deaktivér flydende vindue',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': 'Aktivér flydende vindue',
'permissionTip': 'Tilladelsestip',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/de_de.dart

@ -1113,6 +1113,9 @@ const Map<String, String> deDE = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Schwebendes Fenster deaktivieren',
'pipSettings': 'Untertitelfenster',
'pipSize': 'Größe',
'pipOpacity': 'Deckkraft',
'enableFloatingWindow': 'Schwebendes Fenster aktivieren',
'permissionTip': 'Berechtigungshinweis',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/el_gr.dart

@ -1117,6 +1117,9 @@ const Map<String, String> elGR = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Απενεργοποίηση αιωρούμενου παραθύρου',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': 'Ενεργοποίηση αιωρούμενου παραθύρου',
'permissionTip': 'Υπόδειξη δικαιώματος',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/en_us.dart

@ -1122,6 +1122,9 @@ const Map<String, String> enUS = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Disable Floating Window',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': 'Enable Floating Window',
'permissionTip': 'Permission Tip',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/es_es.dart

@ -1112,6 +1112,9 @@ const Map<String, String> esES = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Desactivar ventana flotante',
'pipSettings': 'Ventana de subtítulos',
'pipSize': 'Tamaño',
'pipOpacity': 'Opacidad',
'enableFloatingWindow': 'Activar ventana flotante',
'permissionTip': 'Aviso de permiso',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/et_ee.dart

@ -1094,6 +1094,9 @@ const Map<String, String> etEE = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Keela ujuvaken',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': 'Luba ujuvaken',
'permissionTip': 'Lubade teade',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/fa_ir.dart

@ -1081,6 +1081,9 @@ const Map<String, String> faIR = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'غیرفعال کردن پنجره شناور',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': 'فعال کردن پنجره شناور',
'permissionTip': 'نکته مجوز',
'micPermissionRequiredForRecording': 'مجوز میکروفون برای ضبط الزامی است',

3
apps/client/lib/core/translations/language/fi_fi.dart

@ -1094,6 +1094,9 @@ const Map<String, String> 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':

3
apps/client/lib/core/translations/language/fr_fr.dart

@ -1123,6 +1123,9 @@ const Map<String, String> 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':

3
apps/client/lib/core/translations/language/hi_in.dart

@ -1093,6 +1093,9 @@ const Map<String, String> hiIN = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'फ़्लोटिंग विंडो अक्षम करें',
'pipSettings': 'उपशीर्षक विंडो',
'pipSize': 'आकार',
'pipOpacity': 'अपारदर्शिता',
'enableFloatingWindow': 'फ़्लोटिंग विंडो सक्षम करें',
'permissionTip': 'अनुमति सुझाव',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/hr_hr.dart

@ -1102,6 +1102,9 @@ const Map<String, String> 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':

3
apps/client/lib/core/translations/language/hu_hu.dart

@ -1106,6 +1106,9 @@ const Map<String, String> huHU = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Lebegő ablak letiltása',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': 'Lebegő ablak engedélyezése',
'permissionTip': 'Engedélyek tipp',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/id_id.dart

@ -1098,6 +1098,9 @@ const Map<String, String> 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',

3
apps/client/lib/core/translations/language/is_is.dart

@ -1096,6 +1096,9 @@ const Map<String, String> 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',

3
apps/client/lib/core/translations/language/it_it.dart

@ -1116,6 +1116,9 @@ const Map<String, String> itIT = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Disattiva finestra mobile',
'pipSettings': 'Finestra sottotitoli',
'pipSize': 'Dimensione',
'pipOpacity': 'Opacità',
'enableFloatingWindow': 'Attiva finestra mobile',
'permissionTip': 'Suggerimento permessi',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/ja_jp.dart

@ -1017,6 +1017,9 @@ const Map<String, String> jaJP = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'フローティングウィンドウを無効化',
'pipSettings': '字幕ウィンドウ',
'pipSize': 'サイズ',
'pipOpacity': '不透明度',
'enableFloatingWindow': 'フローティングウィンドウを有効化',
'permissionTip': '権限のヒント',
'micPermissionRequiredForRecording': '録音にはマイク権限が必要です',

3
apps/client/lib/core/translations/language/ko_kr.dart

@ -1015,6 +1015,9 @@ const Map<String, String> koKR = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': '플로팅 창 비활성화',
'pipSettings': '자막 창',
'pipSize': '크기',
'pipOpacity': '불투명도',
'enableFloatingWindow': '플로팅 창 활성화',
'permissionTip': '권한 안내',
'micPermissionRequiredForRecording': '녹음에는 마이크 권한이 필요합니다',

3
apps/client/lib/core/translations/language/lt_lt.dart

@ -1103,6 +1103,9 @@ const Map<String, String> ltLT = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Išjungti plaukiojantį langą',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': 'Įjungti plaukiojantį langą',
'permissionTip': 'Leidimų patarimas',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/lv_lv.dart

@ -1107,6 +1107,9 @@ const Map<String, String> 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':

3
apps/client/lib/core/translations/language/ms_my.dart

@ -1100,6 +1100,9 @@ const Map<String, String> msMY = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Nyahdayakan Tetingkap Terapung',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': 'Dayakan Tetingkap Terapung',
'permissionTip': 'Petua Kebenaran',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/nb_no.dart

@ -1099,6 +1099,9 @@ const Map<String, String> nbNO = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Deaktiver flytende vindu',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': 'Aktiver flytende vindu',
'permissionTip': 'Tillatelsestips',
'micPermissionRequiredForRecording': 'Mikrofontillatelse kreves for opptak',

3
apps/client/lib/core/translations/language/nl_nl.dart

@ -983,6 +983,9 @@ const Map<String, String> nlNL = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Zwevend venster uitschakelen',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': 'Zwevend venster inschakelen',
'permissionTip': 'Machtigingstip',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/pl_pl.dart

@ -1110,6 +1110,9 @@ const Map<String, String> 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':

3
apps/client/lib/core/translations/language/pt_pt.dart

@ -1117,6 +1117,9 @@ const Map<String, String> ptPT = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Desativar janela flutuante',
'pipSettings': 'Janela de legendas',
'pipSize': 'Tamanho',
'pipOpacity': 'Opacidade',
'enableFloatingWindow': 'Ativar janela flutuante',
'permissionTip': 'Dica de permissao',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/ro_ro.dart

@ -1108,6 +1108,9 @@ const Map<String, String> roRO = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Dezactivare fereastra plutitoare',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': 'Activare fereastra plutitoare',
'permissionTip': 'Sfat privind permisiunile',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/ru_ru.dart

@ -1101,6 +1101,9 @@ const Map<String, String> ruRU = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Отключить плавающее окно',
'pipSettings': 'Окно субтитров',
'pipSize': 'Размер',
'pipOpacity': 'Непрозрачность',
'enableFloatingWindow': 'Включить плавающее окно',
'permissionTip': 'Подсказка о разрешениях',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/sk_sk.dart

@ -1098,6 +1098,9 @@ const Map<String, String> 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':

3
apps/client/lib/core/translations/language/sl_si.dart

@ -1101,6 +1101,9 @@ const Map<String, String> 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':

3
apps/client/lib/core/translations/language/sr_rs.dart

@ -1096,6 +1096,9 @@ const Map<String, String> 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':

3
apps/client/lib/core/translations/language/sv_se.dart

@ -1097,6 +1097,9 @@ const Map<String, String> svSE = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Inaktivera flytande fönster',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': 'Aktivera flytande fönster',
'permissionTip': 'Behörighetstips',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/th_th.dart

@ -1070,6 +1070,9 @@ const Map<String, String> thTH = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'ปิดหน้าต่างลอย',
'pipSettings': 'หน้าต่างคำบรรยาย',
'pipSize': 'ขนาด',
'pipOpacity': 'ความทึบ',
'enableFloatingWindow': 'เปิดหน้าต่างลอย',
'permissionTip': 'คำแนะนำสิทธิ์',
'micPermissionRequiredForRecording':

3
apps/client/lib/core/translations/language/tl_ph.dart

@ -1115,6 +1115,9 @@ const Map<String, String> 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':

3
apps/client/lib/core/translations/language/tr_tr.dart

@ -1094,6 +1094,9 @@ const Map<String, String> 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',

3
apps/client/lib/core/translations/language/uk_ua.dart

@ -1100,6 +1100,9 @@ const Map<String, String> ukUA = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': 'Вимкнути плаваюче вікно',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': 'Увімкнути плаваюче вікно',
'permissionTip': 'Підказка щодо дозволів',
'micPermissionRequiredForRecording': 'Для запису потрібен дозвіл на мікрофон',

3
apps/client/lib/core/translations/language/vi_vn.dart

@ -1086,6 +1086,9 @@ const Map<String, String> 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',

3
apps/client/lib/core/translations/language/zh_cn.dart

@ -1030,6 +1030,9 @@ const Map<String, String> zhCN = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': '禁用悬浮窗',
'pipSettings': '弹幕设置',
'pipSize': '大小',
'pipOpacity': '透明度',
'enableFloatingWindow': '启用悬浮窗',
'permissionTip': '权限提示',
'micPermissionRequiredForRecording': '需要麦克风权限才能使用录音功能',

3
apps/client/lib/core/translations/language/zh_hk.dart

@ -1016,6 +1016,9 @@ const Map<String, String> zhHK = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': '禁用懸浮窗',
'pipSettings': 'Subtitle window',
'pipSize': 'Size',
'pipOpacity': 'Opacity',
'enableFloatingWindow': '啟用懸浮窗',
'permissionTip': '權限提示',
'micPermissionRequiredForRecording': '需要麥克風權限才能使用錄音功能',

3
apps/client/lib/core/translations/language/zh_tw.dart

@ -1016,6 +1016,9 @@ const Map<String, String> zhTW = {
// 翻译模块 - 悬浮窗与权限
'disableFloatingWindow': '停用懸浮視窗',
'pipSettings': '彈幕設定',
'pipSize': '大小',
'pipOpacity': '透明度',
'enableFloatingWindow': '啟用懸浮視窗',
'permissionTip': '權限提示',
'micPermissionRequiredForRecording': '需要麥克風權限才能使用錄音功能',

13
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<double>(_kPipOpacityKey);
final w = _storage.read<double>(_kPipWidthScaleKey);
if (o != null) pipOpacity.value = o.clamp(0.2, 1.0);
if (w != null) pipWidthScale.value = w.clamp(0.5, 2.0);
}
Future<void> 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<void> setPipWidthScale(double v) async {
pipWidthScale.value = v.clamp(0.5, 2.0);
await _storage.write(_kPipWidthScaleKey, pipWidthScale.value);
await SubtitlePip.setStyle(widthScale: pipWidthScale.value);
}

106
apps/client/lib/modules/translation/views/translation_view.dart

@ -30,6 +30,94 @@ enum _TranslationExitAction { continueInBackground, end, cancel }
//主页面
class TranslationView extends GetView<TranslationController> {
/// 字幕小窗(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<void> 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<TranslationController> {
}
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,

51
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,

Loading…
Cancel
Save