diff --git a/CHANGELOG.md b/CHANGELOG.md index 126bbf57..449a53f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,177 @@ 所有重要变更均记录于此文件。格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。 -> 保留最近 14 个版本(v6.101.0 ~ v6.117.0)。更早版本(v6.87.0 ~ v6.100.0)的特性已合并进软件特性功能文档,详见文末归档概览。 +> 保留最近 14 个版本(v6.101.0 ~ v6.120.0)。更早版本(v6.87.0 ~ v6.100.0)的特性已合并进软件特性功能文档,详见文末归档概览。 *** +## [v6.121.0] - 2026-06-24 + +### 🐛 修复(macOS 启动期闪退) + +#### 闪退根因分析 +- **Issue**: macOS 端启动后 3.5 秒闪退,崩溃栈与 v6.119.0 修复前完全一致 +- **崩溃栈**: + - Thread 0 (main): `macos_window_utils` → `[NSWindow setBackgroundColor:]` → `NSThemeFrame._updateBackdropView` → `-[NSView removeFromSuperview]` + - Thread 8 (io.flutter.raster): `impeller::Canvas::SetupRenderPass()` — `KERN_INVALID_ADDRESS at 0x0` +- **根因**: + - `macos_window_utils` 插件的 `MainFlutterWindowManipulator.start()` 方法内部会调用 `showTitle()` / `makeTitlebarOpaque()` / `disableFullSizeContentView()` / `setWindowBackgroundColorToDefaultColor()` 四个方法 + - `setWindowBackgroundColorToDefaultColor()` 会触发 `[NSWindow setBackgroundColor:]` → `NSThemeFrame._updateBackdropView` → `removeFromSuperview` + - Dart 侧 `WindowManipulator.initialize()` → 原生 `reset()` → 当 `mainFlutterWindow == nil` 时走 `start(nil)` 分支 → 触发上述竞态 + - v6.119.0 的修复仅跳过了 Dart 侧 `applyEffect` 中的冗余调用,但**未跳过** `WindowManipulator.initialize()` 内部的 `reset()` → `start(nil)` 路径 +- **修复**: + 1. 在 `MainFlutterWindow.awakeFromNib` 中提前调用 `MainFlutterWindowManipulator.start(mainFlutterWindow: self)`,让 `mainFlutterWindow` 静态属性先被设置 + 2. 之后 `WindowManipulator.initialize()` 的 `reset()` 会因 `mainFlutterWindow != nil` 走 `passthroughViewHandler.start()` 分支,不再触发 `setWindowBackgroundColorToDefaultColor()` + 3. `start()` 会覆盖 `awakeFromNib` 预设的透明属性,因此在 `start()` 之后重新设置(此时 Flutter 引擎未启动,Impeller raster 线程未运行,无竞态风险) +- **涉及文件**: + - `macos/Runner/MainFlutterWindow.swift` — 导入 `macos_window_utils`,提前调用 `start`,之后重新设置透明属性 + +### 🎨 优化(macOS 窗口标题栏 + 系统菜单栏) + +#### 1. macOS 系统菜单栏支持多语言 +- **Issue**: macOS 系统菜单栏(应用菜单/编辑菜单/视图菜单/窗口菜单/帮助菜单)项目仅显示英文,未跟随系统语言 +- **根因**: + - `MainMenu.xib` 只有 `Base.lproj` 版本,无本地化 `.strings` 文件 + - `project.pbxproj` 的 `knownRegions` 仅包含 `en` 和 `Base` +- **修复**: + 1. **创建本地化文件**:为 5 种主要语言创建 `MainMenu.strings`: + - `en.lproj/MainMenu.strings` — 英语 + - `zh-Hans.lproj/MainMenu.strings` — 简体中文 + - `zh-Hant.lproj/MainMenu.strings` — 繁体中文 + - `ja.lproj/MainMenu.strings` — 日语 + - `ko.lproj/MainMenu.strings` — 韩语 + 2. **项目配置**(`project.pbxproj`): + - `knownRegions` 添加 `zh-Hans`/`zh-Hant`/`ja`/`ko` + - 创建 `MainMenu.strings` 的 `PBXVariantGroup` 和 `PBXFileReference` + - 将 `MainMenu.strings` 添加到 `PBXResourcesBuildPhase` +- **效果**: 系统菜单栏跟随 macOS 系统语言显示对应文案 +- **涉及文件**: + - `macos/Runner/en.lproj/MainMenu.strings` — 新增 + - `macos/Runner/zh-Hans.lproj/MainMenu.strings` — 新增 + - `macos/Runner/zh-Hant.lproj/MainMenu.strings` — 新增 + - `macos/Runner/ja.lproj/MainMenu.strings` — 新增 + - `macos/Runner/ko.lproj/MainMenu.strings` — 新增 + - `macos/Runner.xcodeproj/project.pbxproj` — 添加本地化支持 + +#### 2. 去掉视图菜单下的 "Enter Full Screen" 按钮 +- **Issue**: macOS 系统菜单栏 → 视图菜单下有 "Enter Full Screen" 按钮,与 Flutter 侧自绘的绿灯按钮(弹 window size dialog)功能重复 +- **修复**: 删除 `MainMenu.xib` 中 View 菜单下的 "Enter Full Screen" 菜单项 +- **涉及文件**: + - `macos/Runner/Base.lproj/MainMenu.xib` — 删除 "Enter Full Screen" 菜单项 + +#### 3. 软件内切换语言后 macOS 系统菜单栏同步更新 +- **Issue**: 软件内切换语言后,macOS 系统菜单栏( 右边的按钮:应用菜单/编辑菜单/视图菜单/窗口菜单/帮助菜单)未跟随切换,仍显示原语言 +- **根因**: + - macOS 启动时根据系统语言偏好加载 `MainMenu.strings`,软件内切换语言仅更新 Flutter 侧 UI,未通知原生侧重新加载菜单标题 + - `generalSettingsProvider.setLanguage` 方法只更新内存状态 + 持久化 KvStorage + 写日志,未调用任何 MethodChannel + - XIB 加载后 `NSMenuItem.identifier` 默认为 nil,无法通过 XIB objectId 直接定位菜单项 +- **修复**(三层联动): + 1. **原生侧 — 建立 objectId ↔ menuItem 映射**(`MainFlutterWindow.swift`): + - 新增 `menuItemIds` 静态数组,按深度优先顺序记录 XIB 中所有 menuItem 的 objectId(与 `MainMenu.xib` 声明顺序一致,跳过 separatorItem) + - 新增 `assignMenuItemIdentifiers()` 方法,在 `awakeFromNib` 中递归遍历 `NSApp.mainMenu`,按顺序将 objectId 赋值给对应 `menuItem.identifier` + 2. **原生侧 — 切换菜单语言**(`MainFlutterWindow.swift`): + - 新增 `setMenuLanguage(languageId:)` 方法: + - 根据 languageId 解析 lproj 目录名(`system` → `Bundle.preferredLocalizations(from:)` 跟随系统;`zh-CN`/`zh-Hans` → `zh-Hans`;`zh-TW`/`zh-Hant` → `zh-Hant`;`en`/`ja`/`ko` → 对应 lproj) + - 通过 `Bundle.main.path(forResource:ofType:forLocalization:)` 加载对应语言的 `MainMenu.strings` 为 `[String: String]` + - 递归遍历 `NSApp.mainMenu`,根据 `menuItem.identifier`(XIB objectId)查找 `.strings` 中的 key(`"id.title"`)并更新标题 + - 替换 `APP_NAME` 占位符为应用名(`CFBundleName`) + - MethodChannel 新增 `case "setMenuLanguage"` 处理,调用 `self.setMenuLanguage(languageId:)` + 3. **Dart 侧 — 通知原生侧切换**: + - `MacosPlatformService.setMenuLanguage(String languageId)` — 新增静态方法,通过 `apps.xy.xianyan/macos` 通道调用原生侧 + - `generalSettingsProvider.setLanguage` — 在更新状态后调用 `MacosPlatformService.setMenuLanguage(id)` + - `DesktopServicesManager._initMenuLanguage` — 新增方法,应用启动时根据用户设置的语言同步菜单栏(macOS 启动时按系统语言加载 `.strings`,需根据用户设置覆盖) +- **效果**: + - 软件内切换语言后,系统菜单栏所有菜单项标题立即更新为目标语言 + - 应用启动时菜单栏语言与用户设置一致(而非跟随系统语言) +- **涉及文件**: + - `macos/Runner/MainFlutterWindow.swift` — 新增 `menuItemIds`/`assignMenuItemIdentifiers`/`setMenuLanguage`,awakeFromNib 调用 `assignMenuItemIdentifiers` + - `lib/core/services/device/macos_platform_service.dart` — 新增 `setMenuLanguage` 静态方法 + - `lib/features/settings/providers/general_settings_provider.dart` — `setLanguage` 调用 `MacosPlatformService.setMenuLanguage` + - `lib/app/services/desktop_services_manager.dart` — 新增 `_initMenuLanguage`,启动时同步菜单栏语言 + +### 📦 打包(macOS App Store 提交包重新打包) + +#### 1. 生成 App Store 提交用 pkg(x86_64 + arm64 通用二进制) +- **背景**: v6.121.0 修复完成后需重新打包提交 App Store +- **流程**: + 1. `flutter clean` + `flutter pub get` + `pod install` + 2. `flutter build macos --release` — 构建 universal binary (x86_64 + arm64),201.0MB + 3. `xcodebuild archive` — 生成 xcarchive(Automatic 签名,Apple Development 证书) + 4. `xcodebuild -exportArchive` — 导出 App Store pkg,使用 Apple Distribution 证书重新签名 +- **产物**: + - `build/Runner.xcarchive` — 归档文件 + - `build/Runner/闲言.pkg` — 110MB,App Store 提交包 +- **验证**: + - 主可执行文件:universal (x86_64 + arm64) + - pkg 签名:3rd Party Mac Developer Installer: li zhenyang (5V9NVUU6K5) + - app 签名:Apple Distribution: li zhenyang (5V9NVUU6K5) + - Info.plist 包含 `ITSAppUsesNonExemptEncryption=false` 加密合规声明 +- **新增文件**: + - `macos/ExportOptions.plist` — App Store 导出配置(method=app-store, teamID=5V9NVUU6K5) +- **上传方式**: 通过 Xcode Organizer 手动上传(`open -a Xcode-beta build/Runner.xcarchive` → Distribute App → App Store Connect → Upload) +- **注意事项**: + - Release 配置保持 `CODE_SIGN_IDENTITY = "Apple Development"` + `CODE_SIGN_STYLE = Automatic`,exportArchive 阶段会自动使用 Apple Distribution 证书重新签名 + - 若直接将 CODE_SIGN_IDENTITY 改为 "Apple Distribution" 会与 Automatic 签名冲突,导致构建失败 + +--- + +## [v6.120.0] - 2026-06-24 + +### 📦 打包(macOS Universal Binary 提交 App Store) + +#### 1. macOS 包支持 x86_64 + arm64 双架构,用于 App Store 提交 +- **Issue**: 原 macOS Release 配置强制 `ARCHS = arm64`,仅生成 arm64 单架构包,x86_64 Mac 无法原生运行;且 Flutter SDK `xcode_backend.dart` 中存在强制 arm64 的"修复"代码,阻止 universal binary 构建 +- **根因**: + - `macos/Runner.xcodeproj/project.pbxproj` Release 配置中 `ARCHS = arm64` 限制只构建 arm64 + - Flutter SDK `packages/flutter_tools/bin/xcode_backend.dart` 第 646-648 行:当 `ARCHS` 包含空格(多架构)时强制改为 `arm64`,导致 `flutter assemble` 只生成 arm64 的 FlutterMacOS.framework + - Flutter SDK `flutter_tools.snapshot` 未包含 `darwin.dart` 中 `thinFramework` 的逐架构验证修复(snapshot 未随源码更新重新编译) +- **修复**: + 1. **项目配置**(`macos/Runner.xcodeproj/project.pbxproj`):Release 配置 `ARCHS` 从 `arm64` 改为 `$(ARCHS_STANDARD)`,支持 universal binary + 2. **Flutter SDK 修复**(`packages/flutter_tools/bin/xcode_backend.dart`):移除 `if (platform == TargetPlatform.macos && archs.contains(' ')) archs = 'arm64'` 强制 arm64 逻辑,保留多架构传入 `flutter assemble` + 3. **Snapshot 重建**:修改 `bin/cache/flutter_tools.stamp` 触发 `flutter_tools.snapshot` 重新编译,使 `darwin.dart` 的逐架构 `lipo -verify_arch` 验证修复生效 +- **构建结果**: + - `build/macos/Build/Products/Release/xianyan.app` — universal (x86_64 + arm64),200.9MB + - `build/xianyan.xcarchive` — universal archive,用 Apple Distribution 证书签名 + - `build/pkg/闲言.pkg` — 110MB,用 3rd Party Mac Developer Installer 证书签名 +- **验证**: + - 主可执行文件、FlutterMacOS.framework、App.framework 均为 universal (x86_64 + arm64) + - 签名正确:TeamIdentifier=5V9NVUU6K5,Format=app bundle with Mach-O universal + - Info.plist 包含 `ITSAppUsesNonExemptEncryption=false` 加密合规声明 +- **上传方式**: 通过 Xcode Organizer 手动上传(open build/xianyan.xcarchive → Distribute App → App Store Connect → Upload) +- **涉及文件**: + - `macos/Runner.xcodeproj/project.pbxproj` — Release 配置 ARCHS 改为 universal + - Flutter SDK `packages/flutter_tools/bin/xcode_backend.dart` — 移除强制 arm64 逻辑(SDK 本地修改) + - Flutter SDK `bin/cache/flutter_tools.stamp` — 触发 snapshot 重建 +- **注意事项**: + - Flutter SDK 的 `xcode_backend.dart` 修改是本地的,`flutter upgrade` 或重新安装 Flutter SDK 后需重新应用 + - 后续 macOS App Store 打包可直接执行 `flutter build macos --release` + `xcodebuild archive` + `xcodebuild -exportArchive` + +--- + +## [v6.119.0] - 2026-06-24 + +### 🐛 修复(macOS Release 模式启动闪退) + +#### 1. macOS Release 模式启动 ~3.5 秒后 EXC_BAD_ACCESS 空指针崩溃 +- **Issue**: macOS Release 模式启动后约 3.5 秒闪退,Debug 模式正常 +- **崩溃栈**: + - Thread 8 (io.flutter.raster): `impeller::Canvas::SetupRenderPass()` (canvas.cc:1344) — `KERN_INVALID_ADDRESS at 0x0` + - Thread 0 (main): `macos_window_utils` → `[NSWindow setBackgroundColor:]` → `NSThemeFrame _updateBackdropView` → `-[NSView removeFromSuperview]` +- **根因**: 启动期 `applyEffect()` 通过 PostFrame 回调调用 `setWindowBackgroundColorToClear()`,触发 `[NSWindow setBackgroundColor:]` 修改视图层级(`removeFromSuperview`),与 Impeller raster 线程的 `SetupRenderPass` 产生竞态。Release 模式时序更紧凑,竞态更易触发;Debug 模式有 JIT 开销,时序较松散故未复现 +- **修复**(四层防御): + 1. **原生侧前移不变属性**(`MainFlutterWindow.awakeFromNib`):在 Flutter 引擎启动前预设 `isOpaque=false` + `backgroundColor=NSColor.clear` + `titlebarAppearsTransparent=true` + `titleVisibility=.hidden` + `fullSizeContentView`,消除渲染期 `setBackgroundColor:` 调用 + 2. **Dart 侧跳过冗余调用**(`MacosWindowEffectService.applyEffect`):移除 `makeTitlebarTransparent()` / `enableFullSizeContentView()` / `hideTitle()` / `setWindowBackgroundColorToClear()` 四个已在 awakeFromNib 设置的调用,仅保留主题相关的 `overrideMacOSBrightness` 和 `NSVisualEffectView` 操作 + 3. **互斥锁串行化**(`MacosWindowEffectService`):添加 `Completer` 互斥锁,防止 `applyEffect()` 并发执行导致原生视图层级竞争 + 4. **延迟首次应用**(`DesktopServicesManager._initWindowEffect`):首次 `applyEffect()` 延迟 500ms,等待 Impeller 完成初始渲染 + 5. **跳过首次主题同步**(`ThemeSyncService.syncThemeToDesktop`):首次 build 的窗口特效同步由 `_initWindowEffect` 统一处理,避免 build() 与 PostFrame 回调并发触发 `applyEffect()` +- **涉及文件**: + - `macos/Runner/MainFlutterWindow.swift` — awakeFromNib 中预设窗口透明属性 + - `lib/core/services/desktop/implementations/macos_window_effect_service.dart` — 互斥锁 + 跳过冗余调用 + - `lib/app/services/desktop_services_manager.dart` — 延迟首次 applyEffect + - `lib/app/services/theme_sync_service.dart` — 跳过首次窗口特效同步 + +--- + ## [v6.118.0] - 2026-06-24 ### 🐛 修复(macOS 全屏退出后窗口背景变黑) diff --git a/lib/app/services/desktop_services_manager.dart b/lib/app/services/desktop_services_manager.dart index e0ef8d86..245f3434 100644 --- a/lib/app/services/desktop_services_manager.dart +++ b/lib/app/services/desktop_services_manager.dart @@ -3,7 +3,8 @@ /// 创建时间: 2026-06-22 /// 更新时间: 2026-06-24 /// 作用: 统一管理桌面端服务初始化和生命周期(托盘、窗口特效、菜单栏) -/// 上次更新: 注册 macOS 全屏退出事件回调,退出后重新应用窗口特效 +/// 上次更新: 新增 _initMenuLanguage,应用启动时根据用户设置的语言同步 +/// macOS 系统菜单栏标题(NSApp.mainMenu) /// ============================================================ import 'package:flutter/widgets.dart'; @@ -15,6 +16,7 @@ import '../../core/services/device/macos_platform_service.dart'; import '../../core/utils/logger.dart'; import '../../core/utils/platform/platform_utils.dart' as pu; import '../../features/desktop/desktop_tray_controller.dart'; +import '../../features/settings/providers/general_settings_provider.dart'; import '../../features/settings/providers/theme_settings_provider.dart'; /// 桌面端服务管理器 @@ -67,6 +69,10 @@ class DesktopServicesManager { // 2c. 注册 macOS 原生事件监听(全屏退出等) _initNativeEventListeners(); + + // 2d. 同步 macOS 系统菜单栏语言 + // 应用启动时 macOS 会按系统语言加载 .strings,需根据用户设置同步 + _initMenuLanguage(); } catch (e, st) { Log.e('桌面端服务初始化失败', e, st); } @@ -74,6 +80,9 @@ class DesktopServicesManager { } /// 初始化窗口特效服务并应用当前主题 + /// + /// 延迟 500ms 等待 Impeller 完成初始渲染后再应用特效, + /// 避免 NSVisualEffectView 子视图添加与 Impeller 首帧渲染产生竞态。 Future _initWindowEffect() async { try { final effectService = DesktopWindowEffectService.instance; @@ -84,6 +93,10 @@ class DesktopServicesManager { await effectService.initialize(); + // 延迟等待 Impeller 完成初始渲染,避免 NSVisualEffectView 操作与 + // raster 线程的 SetupRenderPass 产生竞态导致空指针崩溃 + await Future.delayed(const Duration(milliseconds: 500)); + final isDark = _ref.read(themeSettingsProvider).isDark; await effectService.applyEffect(isDark: isDark); @@ -106,6 +119,23 @@ class DesktopServicesManager { Log.i('macOS 原生事件监听已注册'); } + /// 同步 macOS 系统菜单栏语言 + /// + /// 应用启动时 macOS 会按系统语言偏好加载 MainMenu.strings, + /// 若用户在软件内设置了非系统语言,需调用此方法同步菜单栏标题。 + /// 仅 macOS 生效,其他平台自动跳过。 + void _initMenuLanguage() { + if (!pu.isMacOS) return; + + try { + final languageId = _ref.read(generalSettingsProvider).languageId; + MacosPlatformService.setMenuLanguage(languageId); + Log.i('macOS 系统菜单栏语言已同步: $languageId'); + } catch (e, st) { + Log.e('macOS 系统菜单栏语言同步失败', e, st); + } + } + /// 全屏退出后重新应用窗口特效 /// /// 原生侧已立即重置 `isOpaque=false` + `backgroundColor=clear`, diff --git a/lib/app/services/theme_sync_service.dart b/lib/app/services/theme_sync_service.dart index 3871786c..0e574157 100644 --- a/lib/app/services/theme_sync_service.dart +++ b/lib/app/services/theme_sync_service.dart @@ -33,6 +33,14 @@ class ThemeSyncService { /// 仅靠 [_lastDesktopDarkState] 无法检测 dark↔amoled 切换。 bool? _lastDesktopAmoledState; + /// 是否已跳过首次窗口特效同步 + /// + /// 首次窗口特效由 [DesktopServicesManager._initWindowEffect] 统一处理 + /// (含 500ms 延迟等待 Impeller 完成初始渲染)。 + /// 此处跳过首次同步,避免 build() 与 PostFrame 回调并发触发 applyEffect, + /// 导致 NSWindow 属性修改与 Impeller raster 线程竞态崩溃。 + bool _windowEffectFirstSyncSkipped = false; + /// 同步主题到桌面端原生平台 /// /// 仅在状态变化时同步,避免重复调用。 @@ -61,12 +69,19 @@ class ThemeSyncService { _lastDesktopDarkState = isDark; _lastDesktopAmoledState = isAmoled; - // 同步窗口特效 - DesktopWindowEffectService.instance - .applyEffect(isDark: isDark) - .catchError((Object e) { - Log.e('窗口特效主题同步失败', e); - }); + // 跳过首次窗口特效同步:由 DesktopServicesManager._initWindowEffect 统一处理 + // (含 500ms 延迟等待 Impeller 完成初始渲染),避免启动期并发调用导致竞态崩溃 + if (!_windowEffectFirstSyncSkipped) { + _windowEffectFirstSyncSkipped = true; + Log.i('跳过首次窗口特效同步,由 _initWindowEffect 统一处理'); + } else { + // 同步窗口特效 + DesktopWindowEffectService.instance + .applyEffect(isDark: isDark) + .catchError((Object e) { + Log.e('窗口特效主题同步失败', e); + }); + } // 同步托盘图标主题 if (trayController != null) { @@ -113,5 +128,6 @@ class ThemeSyncService { void reset() { _lastDesktopDarkState = null; _lastDesktopAmoledState = null; + _windowEffectFirstSyncSkipped = false; } } diff --git a/lib/core/services/desktop/desktop_tray_service.dart b/lib/core/services/desktop/desktop_tray_service.dart index 6698847d..19635a08 100644 --- a/lib/core/services/desktop/desktop_tray_service.dart +++ b/lib/core/services/desktop/desktop_tray_service.dart @@ -1,3 +1,4 @@ + /// ============================================================ /// 闲言APP — 桌面端系统托盘服务抽象 /// 创建时间: 2026-06-18 diff --git a/lib/core/services/desktop/implementations/macos_window_effect_service.dart b/lib/core/services/desktop/implementations/macos_window_effect_service.dart index 600cd089..bc5073f2 100644 --- a/lib/core/services/desktop/implementations/macos_window_effect_service.dart +++ b/lib/core/services/desktop/implementations/macos_window_effect_service.dart @@ -1,11 +1,15 @@ /// ============================================================ /// 闲言APP — macOS 窗口特效服务实现 /// 创建时间: 2026-06-18 -/// 更新时间: 2026-06-18 +/// 更新时间: 2026-06-24 /// 作用: 基于 macos_window_utils 实现 macOS 窗口特效(标题栏融合/侧边栏毛玻璃) -/// 上次更新: 初始创建,实现 DesktopWindowEffectService 接口 +/// 上次更新: 修复 Release 模式闪退 — 添加互斥锁串行化 applyEffect 调用, +/// 跳过已在 MainFlutterWindow.awakeFromNib 中预设的不变属性调用, +/// 避免 [NSWindow setBackgroundColor:] 与 Impeller SetupRenderPass 竞态 /// ============================================================ +import 'dart:async'; + import 'package:macos_window_utils/macos_window_utils.dart'; import '../desktop_window_effect_service.dart'; @@ -18,6 +22,10 @@ import 'package:xianyan/core/utils/logger.dart'; /// - 标题栏融合(titlebarAppearsTransparent + fullSizeContentView) /// - 侧边栏毛玻璃(NSVisualEffectViewMaterial.sidebar) /// - 主题跟随(overrideMacOSBrightness) +/// +/// 不变属性(isOpaque/backgroundColor/titlebarAppearsTransparent/fullSizeContentView/ +/// titleVisibility)已在 [MainFlutterWindow.awakeFromNib] 中预设, +/// 此处不再通过 method channel 重复设置,避免与 Impeller 渲染线程竞态。 class MacosWindowEffectService implements DesktopWindowEffectService { MacosWindowEffectService._(); @@ -29,6 +37,9 @@ class MacosWindowEffectService implements DesktopWindowEffectService { bool _initialized = false; int? _sidebarVisualEffectSubviewId; + /// 互斥锁:串行化 applyEffect 调用,防止并发导致原生视图层级竞争 + Completer? _applyLock; + @override Future initialize() async { if (!pu.isMacOS) return; @@ -50,26 +61,41 @@ class MacosWindowEffectService implements DesktopWindowEffectService { }) async { if (!pu.isMacOS || !_initialized) return; - try { - // 1. 标题栏融合:透明标题栏 + 全尺寸内容视图 - await WindowManipulator.makeTitlebarTransparent(); - await WindowManipulator.enableFullSizeContentView(); - await WindowManipulator.hideTitle(); + // 串行化:若上一次 applyEffect 尚未完成,等待其结束后再执行 + while (_applyLock != null) { + try { + await _applyLock!.future; + } catch (_) { + break; + } + } + _applyLock = Completer(); - // 2. 主题跟随:覆盖 macOS 亮度设置 + try { + // 注意:以下属性已在 MainFlutterWindow.awakeFromNib 中预设,此处不再重复调用: + // - makeTitlebarTransparent() → titlebarAppearsTransparent = true + // - enableFullSizeContentView() → styleMask.insert(.fullSizeContentView) + // - hideTitle() → titleVisibility = .hidden + // - setWindowBackgroundColorToClear() → isOpaque=false + backgroundColor=clear + // + // 这些调用会触发 [NSWindow setBackgroundColor:] → NSThemeFrame._updateBackdropView + // → removeFromSuperview,与 Impeller raster 线程的 SetupRenderPass 产生竞态, + // 导致 EXC_BAD_ACCESS 空指针崩溃(Release 模式时序更紧更易触发)。 + + // 1. 主题跟随:覆盖 macOS 亮度设置(仅修改 appearance,不触发 _updateBackdropView) await WindowManipulator.overrideMacOSBrightness(dark: isDark); - // 3. 侧边栏毛玻璃 + // 2. 侧边栏毛玻璃(NSVisualEffectView 操作,不修改 NSWindow 背景) if (sidebarBlur) { await _applySidebarBlur(); } - // 4. 窗口背景色设为透明(让 NSVisualEffectView 透出) - await WindowManipulator.setWindowBackgroundColorToClear(); - Log.i('MacosWindowEffectService 特效已应用 (isDark=$isDark, sidebarBlur=$sidebarBlur)'); } catch (e) { Log.e('MacosWindowEffectService.applyEffect 失败: $e'); + } finally { + _applyLock?.complete(); + _applyLock = null; } } diff --git a/lib/core/services/device/macos_platform_service.dart b/lib/core/services/device/macos_platform_service.dart index 7af69eaa..8d3bbfc0 100644 --- a/lib/core/services/device/macos_platform_service.dart +++ b/lib/core/services/device/macos_platform_service.dart @@ -2,8 +2,8 @@ /// 闲言APP — macOS平台统一服务 /// 创建时间: 2026-06-02 /// 更新时间: 2026-06-24 -/// 作用: 集中管理所有macOS原生MethodChannel交互(主题同步/窗口管理/Touch Bar/共享/Dock徽章/菜单栏金句/Spotlight) -/// 上次更新: 新增原生事件监听(onFullScreenExited),支持全屏退出后重新应用窗口特效 +/// 作用: 集中管理所有macOS原生MethodChannel交互(主题同步/窗口管理/Touch Bar/共享/Dock徽章/菜单栏金句/Spotlight/菜单语言) +/// 上次更新: 新增 setMenuLanguage 方法,支持软件内切换语言后同步更新 macOS 系统菜单栏 /// ============================================================ import 'package:flutter/services.dart'; @@ -238,6 +238,31 @@ class MacosPlatformService { } } + // ============================================================ + // 菜单栏语言切换 + // ============================================================ + + /// 通知原生侧切换 macOS 系统菜单栏语言 + /// + /// [languageId] 语言 ID,对应 macOS 本地化区域: + /// - 'system' → 跟随系统语言 + /// - 'zh-CN' / 'zh-Hans' → 简体中文 + /// - 'zh-TW' / 'zh-Hant' → 繁体中文 + /// - 'en' → 英语 + /// - 'ja' → 日语 + /// - 'ko' → 韩语 + /// + /// 软件内切换语言后调用此方法,原生侧会重新加载对应语言的 MainMenu.strings + /// 并更新 NSApp.mainMenu 的所有菜单项标题。 + static Future setMenuLanguage(String languageId) async { + if (!pu.isMacOS) return; + try { + await _channel.invokeMethod('setMenuLanguage', {'languageId': languageId}); + } catch (e) { + Log.w('MacosPlatformService.setMenuLanguage失败: $e'); + } + } + // ============================================================ // 内部工具 // ============================================================ diff --git a/lib/features/settings/providers/general_settings_provider.dart b/lib/features/settings/providers/general_settings_provider.dart index 484f56ba..adeb3bf3 100644 --- a/lib/features/settings/providers/general_settings_provider.dart +++ b/lib/features/settings/providers/general_settings_provider.dart @@ -14,6 +14,7 @@ import 'package:path_provider/path_provider.dart'; import '../../../core/services/audio/sfx_service.dart'; import '../../../core/services/device/haptic_service.dart'; +import '../../../core/services/device/macos_platform_service.dart'; import '../../../core/services/device/screen_wake_service.dart'; import '../../../core/services/ui/status_bar_service.dart'; import '../../../core/services/sound_service.dart' @@ -538,6 +539,9 @@ class GeneralSettingsNotifier extends Notifier { state = state.copyWith(general: state.general.copyWith(languageId: id)); KvStorage.setString('${_keyPrefix}language', id); SettingsChangeLogger.log('language', old, id); + // 通知 macOS 原生侧切换系统菜单栏语言 + // 软件内切换语言后,NSApp.mainMenu 的菜单项标题需同步更新 + MacosPlatformService.setMenuLanguage(id); } void setImmersiveStatusBar(bool v) { diff --git a/macos/ExportOptions.plist b/macos/ExportOptions.plist new file mode 100644 index 00000000..c27ca7eb --- /dev/null +++ b/macos/ExportOptions.plist @@ -0,0 +1,14 @@ + + + + + method + app-store + teamID + 5V9NVUU6K5 + uploadSymbols + + uploadBitcode + + + diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index a2f166f4..d07295b6 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 69AC2FD7F25C22464C6AA971 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5EAD42DE88C17A1D7496A378 /* Pods_RunnerTests.framework */; }; 740419A3DA2BFE0F5E85D9AF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9097D4D2EB98F14D8827394A /* Pods_Runner.framework */; }; + A1B2C3D4E5F6000000000007 /* MainMenu.strings in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6000000000006 /* MainMenu.strings */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -66,7 +67,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* s2ss.com */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = s2ss.com; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* xianyan.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = xianyan.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -84,6 +85,11 @@ 9097D4D2EB98F14D8827394A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; 9DF906408CB20CD31706ACF0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + A1B2C3D4E5F6000000000001 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = ""; }; + A1B2C3D4E5F6000000000002 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/MainMenu.strings"; sourceTree = ""; }; + A1B2C3D4E5F6000000000003 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/MainMenu.strings"; sourceTree = ""; }; + A1B2C3D4E5F6000000000004 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/MainMenu.strings; sourceTree = ""; }; + A1B2C3D4E5F6000000000005 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/MainMenu.strings; sourceTree = ""; }; CC173CB9C4D834544F95E13C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; E124C85A506C3CD1E10E0BF4 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; F3F87547C10EAEA1CDC2EAC9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; @@ -144,7 +150,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* s2ss.com */, + 33CC10ED2044A3C60003C045 /* xianyan.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; @@ -155,6 +161,7 @@ children = ( 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, + A1B2C3D4E5F6000000000006 /* MainMenu.strings */, 33CC10F72044A3C60003C045 /* Info.plist */, ); name = Resources; @@ -195,7 +202,6 @@ 475082748095A981E9B2017D /* Pods-RunnerTests.release.xcconfig */, 9DF906408CB20CD31706ACF0 /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -249,7 +255,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* s2ss.com */; + productReference = 33CC10ED2044A3C60003C045 /* xianyan.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -289,6 +295,10 @@ hasScannedForEncodings = 0; knownRegions = ( en, + "zh-Hans", + "zh-Hant", + ja, + ko, Base, ); mainGroup = 33CC10E42044A3C60003C045; @@ -317,6 +327,7 @@ files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + A1B2C3D4E5F6000000000007 /* MainMenu.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -468,6 +479,19 @@ path = Runner; sourceTree = ""; }; + A1B2C3D4E5F6000000000006 /* MainMenu.strings */ = { + isa = PBXVariantGroup; + children = ( + A1B2C3D4E5F6000000000001 /* en */, + A1B2C3D4E5F6000000000002 /* zh-Hans */, + A1B2C3D4E5F6000000000003 /* zh-Hant */, + A1B2C3D4E5F6000000000004 /* ja */, + A1B2C3D4E5F6000000000005 /* ko */, + ); + name = MainMenu.strings; + path = Runner; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -729,12 +753,12 @@ isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - ARCHS = arm64; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 5V9NVUU6K5; INFOPLIST_FILE = Runner/Info.plist; diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib index 80e867a4..ee571fca 100644 --- a/macos/Runner/Base.lproj/MainMenu.xib +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -289,12 +289,6 @@ - - - - - - diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 8d65a073..2add2fad 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -4,6 +4,14 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleLocalizations + + en + zh-Hans + zh-Hant + ja + ko + CFBundleExecutable $(EXECUTABLE_NAME) CFBundleDisplayName diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift index 44750f9c..88c76c1b 100644 --- a/macos/Runner/MainFlutterWindow.swift +++ b/macos/Runner/MainFlutterWindow.swift @@ -3,11 +3,24 @@ /// 创建时间: 2026-06-02 /// 更新时间: 2026-06-24 /// 作用: 主窗口初始化,注册 apps.xy.xianyan/macos MethodChannel -/// 上次更新: 监听全屏退出事件,修复退出全屏后窗口背景变黑的问题 +/// 上次更新: 实现菜单栏语言切换 — 新增 menuItemIds 静态数组(XIB objectId 列表)、 +/// assignMenuItemIdentifiers(为每个 menuItem 设置 identifier)、 +/// setMenuLanguage(根据 languageId 加载对应 lproj/MainMenu.strings +/// 并递归更新 NSApp.mainMenu 所有菜单项标题)。 +/// 修复软件内切换语言后系统菜单栏不跟随切换的问题。 +/// 同时保留先前修复:在 awakeFromNib 提前调用 +/// MainFlutterWindowManipulator.start(mainFlutterWindow: self) +/// 避免 reset() 走 start(nil) 分支触发竞态崩溃; +/// contentViewController 由 FlutterViewController 改为 +/// MacOSWindowUtilsViewController 包装,使原生侧 setMaterial/ +/// setNSVisualEffectViewState 等方法的类型转换成功。 +/// 通过 setExtraFlag 保留 fullSizeContentView + titlebarAppearsTransparent +/// 实现标题栏融合(红黄绿灯由 Flutter 侧 DesktopWindowTitleBar 自绘) /// ============================================================ import Cocoa import FlutterMacOS +import macos_window_utils class MainFlutterWindow: NSWindow, NSTouchBarDelegate { // ============================================================ @@ -23,9 +36,63 @@ class MainFlutterWindow: NSWindow, NSTouchBarDelegate { override func awakeFromNib() { let flutterViewController = FlutterViewController() let windowFrame = self.frame - self.contentViewController = flutterViewController + + // 关键修复:使用 MacOSWindowUtilsViewController 包装 FlutterViewController + // macos_window_utils 插件的原生方法(setMaterial/setNSVisualEffectViewState/ + // addVisualEffectSubview 等)内部执行 + // `contentViewController as! MacOSWindowUtilsViewController`, + // 若直接使用 FlutterViewController 作为 contentViewController 会触发 + // "Could not cast value of type 'FlutterViewController' to + // 'MacOSWindowUtilsViewController'" 崩溃。 + // MacOSWindowUtilsViewController.loadView() 会创建 NSVisualEffectView 作为 + // 根视图并 addChild(flutterViewController),是侧边栏毛玻璃等特效的载体。 + let macOSWindowUtilsViewController = MacOSWindowUtilsViewController( + flutterViewController: flutterViewController + ) + self.contentViewController = macOSWindowUtilsViewController self.setFrame(windowFrame, display: true) + // 在 Flutter 引擎启动前预设窗口透明属性,消除 Impeller 渲染期竞态 + // 根因:macos_window_utils 的 setWindowBackgroundColorToClear() 在渲染期间调用 + // [NSWindow setBackgroundColor:] → NSThemeFrame._updateBackdropView → removeFromSuperview, + // 与 Impeller raster 线程的 SetupRenderPass 产生竞态,导致空指针崩溃(Release 模式时序更紧)。 + // 修复:将不变属性前移到引擎启动前设置,Dart 侧不再在渲染期重复调用。 + self.isOpaque = false + self.backgroundColor = NSColor.clear + self.titlebarAppearsTransparent = true + self.titleVisibility = .hidden + if !self.styleMask.contains(.fullSizeContentView) { + self.styleMask.insert(.fullSizeContentView) + } + + // 关键修复:提前调用 MainFlutterWindowManipulator.start(mainFlutterWindow: self) + // 让 macos_window_utils 插件的 mainFlutterWindow 静态属性先被设置。 + // 否则后续 Dart 侧 WindowManipulator.initialize() → 原生 reset() 会因 + // mainFlutterWindow == nil 走 start(nil) 分支,触发 + // setWindowBackgroundColorToDefaultColor() → [NSWindow setBackgroundColor:] + // → NSThemeFrame._updateBackdropView → removeFromSuperview, + // 与 Impeller raster 线程的 SetupRenderPass 产生竞态导致 EXC_BAD_ACCESS 崩溃。 + // + // 注意:start() 内部会调用 showTitle()/makeTitlebarOpaque()/disableFullSizeContentView()/ + // setWindowBackgroundColorToDefaultColor() 覆盖上述预设属性,因此必须在 start() 之后 + // 重新设置透明属性。此时 Flutter 引擎尚未启动,Impeller raster 线程未运行, + // [NSWindow setBackgroundColor:] 不会与渲染线程竞态,安全无崩溃。 + // + // 由于此处传入 mainFlutterWindow: self,start() 内部 isProvidedWindow=true, + // 不会调用 configureMainFlutterWindow()(该方法会再次包装 contentViewController, + // 但因已是 MacOSWindowUtilsViewController,重复包装会导致 FlutterViewController + // 被嵌套两层,引发布局异常)。当前方案在上方已正确包装,无需 configure。 + MainFlutterWindowManipulator.start(mainFlutterWindow: self) + + // start() 覆盖后重新设置透明属性(引擎启动前,无竞态风险) + self.isOpaque = false + self.backgroundColor = NSColor.clear + self.titlebarAppearsTransparent = true + self.titleVisibility = .hidden + if !self.styleMask.contains(.fullSizeContentView) { + self.styleMask.insert(.fullSizeContentView) + } + RegisterGeneratedPlugins(registry: flutterViewController) // 在 Flutter 引擎启动前注册自定义 MethodChannel,避免 MissingPluginException @@ -40,6 +107,11 @@ class MainFlutterWindow: NSWindow, NSTouchBarDelegate { object: self ) + // 为 mainMenu 中每个 menuItem 设置 identifier(XIB objectId) + // 以便 setMenuLanguage 能根据 identifier 查找 .strings 中的本地化标题 + // 必须在 NSApp.mainMenu 从 XIB 加载后调用(awakeFromNib 时 mainMenu 已存在) + self.assignMenuItemIdentifiers() + super.awakeFromNib() } @@ -204,6 +276,13 @@ class MainFlutterWindow: NSWindow, NSTouchBarDelegate { } result(nil) + // ---------- 菜单栏语言切换 ---------- + case "setMenuLanguage": + let args = call.arguments as? [String: Any] ?? [:] + let languageId = (args["languageId"] as? String) ?? "system" + self.setMenuLanguage(languageId: languageId) + result(nil) + default: result(FlutterMethodNotImplemented) } @@ -299,6 +378,159 @@ class MainFlutterWindow: NSWindow, NSTouchBarDelegate { ) } + // ============================================================ + // MARK: - 菜单栏语言切换实现 + // ============================================================ + + /// XIB 中所有 menuItem 的 objectId,按深度优先遍历顺序排列 + /// 用于在 awakeFromNib 中为每个 menuItem 设置 identifier + /// 顺序:顶层菜单项 → 其子菜单项(跳过 separatorItem) + /// 与 MainMenu.xib 中 menuItem 的声明顺序一致 + private static let menuItemIds: [String] = [ + // App Menu(顶层 + 子菜单项) + "1Xt-HY-uBw", "5kV-Vb-QxS", "BOF-NM-1cW", "NMo-om-nkz", + "Olw-nP-bQN", "Vdr-fp-XzO", "Kd2-mp-pUS", "4sb-4s-VLi", + // Edit Menu(顶层 + 子菜单项) + "5QF-Oa-p0T", "dRJ-4n-Yzg", "6dh-zS-Vam", "uRl-iY-unG", + "x3v-GG-iWU", "gVA-U4-sdL", "WeT-3V-zwk", "pa3-QI-u2k", + "Ruw-6m-B2m", + // Edit > Find + "4EN-yA-p0u", "Xz5-n4-O0W", "YEy-JH-Tfz", "q09-fT-Sye", + "OwM-mh-QMV", "buJ-ug-pKt", "S0p-oC-mLd", + // Edit > Spelling and Grammar + "Dv1-io-Yv7", "HFo-cy-zxI", "hz2-CU-CR7", "rbD-Rh-wIN", + "mK6-2p-4JG", "78Y-hA-62v", + // Edit > Substitutions + "9ic-FL-obx", "z6F-FW-3nz", "9yt-4B-nSM", "hQb-2v-fYv", + "rgM-f4-ycn", "cwL-P1-jid", "tRr-pd-1PS", "HFQ-gK-NFA", + // Edit > Transformations + "2oI-Rn-ZJC", "vmV-6d-7jI", "d9M-CD-aMd", "UEZ-Bs-lqG", + // Edit > Speech + "xrE-MZ-jX0", "Ynk-f8-cLZ", "Oyz-dy-DGm", + // View Menu(顶层,子菜单项为空) + "H8h-7b-M4v", + // Window Menu(顶层 + 子菜单项) + "aUF-d1-5bR", "OY7-WF-poV", "R4o-n2-Eq4", "LE2-aR-0XJ", + // Help Menu(顶层,无子菜单项) + "EPT-qC-fAb", + ] + + /// 为 mainMenu 中每个 menuItem 设置 identifier(XIB objectId) + /// + /// XIB 加载后,menuItem.identifier 默认为 nil,无法通过 objectId 查找。 + /// 此方法按深度优先顺序遍历 mainMenu,将 menuItemIds 中的 objectId + /// 依次赋值给对应 menuItem.identifier,建立 objectId ↔ menuItem 映射。 + /// 跳过 separatorItem(XIB 中 separator 无 objectId)。 + private func assignMenuItemIdentifiers() { + var index = 0 + func assign(_ menu: NSMenu) { + for item in menu.items { + if item.isSeparatorItem { continue } + if index < MainFlutterWindow.menuItemIds.count { + item.identifier = NSUserInterfaceItemIdentifier( + rawValue: MainFlutterWindow.menuItemIds[index] + ) + index += 1 + } + if let submenu = item.submenu { + assign(submenu) + } + } + } + guard let mainMenu = NSApp.mainMenu else { + NSLog("[assignMenuItemIdentifiers] NSApp.mainMenu 为 nil") + return + } + assign(mainMenu) + NSLog("[assignMenuItemIdentifiers] 已为 \(index) 个菜单项设置 identifier") + } + + /// 切换 macOS 系统菜单栏语言 + /// + /// [languageId] 语言 ID: + /// - 'system' / 'auto' → 跟随系统语言(使用 preferredLocalizations) + /// - 'zh-CN' / 'zh-Hans' → 简体中文 + /// - 'zh-TW' / 'zh-Hant' → 繁体中文 + /// - 'en' → 英语 + /// - 'ja' → 日语 + /// - 'ko' → 韩语 + /// + /// 实现原理: + /// 1. 根据 languageId 解析对应的 lproj 目录名 + /// 2. 加载对应语言的 MainMenu.strings 文件为 [String: String] + /// 3. 递归遍历 NSApp.mainMenu,根据 menuItem.identifier(XIB objectId) + /// 查找 .strings 中的 key("id.title")并更新标题 + /// 4. 替换 "APP_NAME" 占位符为应用名(CFBundleName) + /// + /// 注意:此方法仅更新菜单项标题,不重建菜单结构,不影响 action 连接。 + private func setMenuLanguage(languageId: String) { + // 1. 解析 lproj 目录名 + let lprojName: String + switch languageId { + case "system", "auto": + // preferredLocalizations(from:) 是 Bundle 类的静态方法, + // 返回系统首选语言在给定列表中的匹配顺序 + let preferred = Bundle.preferredLocalizations(from: [ + "en", "zh-Hans", "zh-Hant", "ja", "ko", + ]) + lprojName = preferred.first ?? "en" + case "zh-CN", "zh-Hans": + lprojName = "zh-Hans" + case "zh-TW", "zh-Hant": + lprojName = "zh-Hant" + case "en": + lprojName = "en" + case "ja": + lprojName = "ja" + case "ko": + lprojName = "ko" + default: + // 未知语言,回退到英语 + lprojName = "en" + } + + // 2. 加载对应语言的 MainMenu.strings + guard let stringsPath = Bundle.main.path( + forResource: "MainMenu", + ofType: "strings", + inDirectory: nil, + forLocalization: lprojName + ) else { + NSLog("[setMenuLanguage] 未找到 \(lprojName).lproj/MainMenu.strings") + return + } + guard let strings = NSDictionary(contentsOfFile: stringsPath) as? [String: String] else { + NSLog("[setMenuLanguage] 加载 \(lprojName).lproj/MainMenu.strings 失败") + return + } + + // 3. 递归遍历 mainMenu,更新标题 + let appName = (Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String) ?? "闲言" + var updatedCount = 0 + func updateMenu(_ menu: NSMenu) { + for item in menu.items { + if item.isSeparatorItem { continue } + if let identifier = item.identifier?.rawValue { + let key = "\(identifier).title" + if let localized = strings[key] { + // 替换 APP_NAME 占位符为应用名 + item.title = localized.replacingOccurrences(of: "APP_NAME", with: appName) + updatedCount += 1 + } + } + if let submenu = item.submenu { + updateMenu(submenu) + } + } + } + guard let mainMenu = NSApp.mainMenu else { + NSLog("[setMenuLanguage] NSApp.mainMenu 为 nil") + return + } + updateMenu(mainMenu) + NSLog("[setMenuLanguage] 菜单栏语言已切换为 \(lprojName),更新 \(updatedCount) 个菜单项") + } + // ============================================================ // MARK: - Touch Bar 实现 // ============================================================ diff --git a/macos/Runner/en.lproj/MainMenu.strings b/macos/Runner/en.lproj/MainMenu.strings new file mode 100644 index 00000000..a43e2cb9 --- /dev/null +++ b/macos/Runner/en.lproj/MainMenu.strings @@ -0,0 +1,76 @@ +/* ============================================================ + * 闲言APP — macOS 主菜单本地化(英语) + * 创建时间: 2026-06-24 + * 作用: macOS 系统菜单栏英文文案 + * ============================================================ */ + +/* ===== App Menu ===== */ +"1Xt-HY-uBw.title" = "xianyan"; +"5kV-Vb-QxS.title" = "About xianyan"; +"BOF-NM-1cW.title" = "Preferences…"; +"NMo-om-nkz.title" = "Services"; +"Olw-nP-bQN.title" = "Hide xianyan"; +"Vdr-fp-XzO.title" = "Hide Others"; +"Kd2-mp-pUS.title" = "Show All"; +"4sb-4s-VLi.title" = "Quit xianyan"; + +/* ===== Edit Menu ===== */ +"5QF-Oa-p0T.title" = "Edit"; +"dRJ-4n-Yzg.title" = "Undo"; +"6dh-zS-Vam.title" = "Redo"; +"uRl-iY-unG.title" = "Cut"; +"x3v-GG-iWU.title" = "Copy"; +"gVA-U4-sdL.title" = "Paste"; +"WeT-3V-zwk.title" = "Paste and Match Style"; +"pa3-QI-u2k.title" = "Delete"; +"Ruw-6m-B2m.title" = "Select All"; + +/* ===== Edit > Find ===== */ +"4EN-yA-p0u.title" = "Find"; +"Xz5-n4-O0W.title" = "Find…"; +"YEy-JH-Tfz.title" = "Find and Replace…"; +"q09-fT-Sye.title" = "Find Next"; +"OwM-mh-QMV.title" = "Find Previous"; +"buJ-ug-pKt.title" = "Use Selection for Find"; +"S0p-oC-mLd.title" = "Jump to Selection"; + +/* ===== Edit > Spelling and Grammar ===== */ +"Dv1-io-Yv7.title" = "Spelling and Grammar"; +"HFo-cy-zxI.title" = "Show Spelling and Grammar"; +"hz2-CU-CR7.title" = "Check Document Now"; +"rbD-Rh-wIN.title" = "Check Spelling While Typing"; +"mK6-2p-4JG.title" = "Check Grammar With Spelling"; +"78Y-hA-62v.title" = "Correct Spelling Automatically"; + +/* ===== Edit > Substitutions ===== */ +"9ic-FL-obx.title" = "Substitutions"; +"z6F-FW-3nz.title" = "Show Substitutions"; +"9yt-4B-nSM.title" = "Smart Copy/Paste"; +"hQb-2v-fYv.title" = "Smart Quotes"; +"rgM-f4-ycn.title" = "Smart Dashes"; +"cwL-P1-jid.title" = "Smart Links"; +"tRr-pd-1PS.title" = "Data Detectors"; +"HFQ-gK-NFA.title" = "Text Replacement"; + +/* ===== Edit > Transformations ===== */ +"2oI-Rn-ZJC.title" = "Transformations"; +"vmV-6d-7jI.title" = "Make Upper Case"; +"d9M-CD-aMd.title" = "Make Lower Case"; +"UEZ-Bs-lqG.title" = "Capitalize"; + +/* ===== Edit > Speech ===== */ +"xrE-MZ-jX0.title" = "Speech"; +"Ynk-f8-cLZ.title" = "Start Speaking"; +"Oyz-dy-DGm.title" = "Stop Speaking"; + +/* ===== View Menu ===== */ +"H8h-7b-M4v.title" = "View"; + +/* ===== Window Menu ===== */ +"aUF-d1-5bR.title" = "Window"; +"OY7-WF-poV.title" = "Minimize"; +"R4o-n2-Eq4.title" = "Zoom"; +"LE2-aR-0XJ.title" = "Bring All to Front"; + +/* ===== Help Menu ===== */ +"EPT-qC-fAb.title" = "Help"; diff --git a/macos/Runner/ja.lproj/MainMenu.strings b/macos/Runner/ja.lproj/MainMenu.strings new file mode 100644 index 00000000..79fd5b18 --- /dev/null +++ b/macos/Runner/ja.lproj/MainMenu.strings @@ -0,0 +1,76 @@ +/* ============================================================ + * 闲言APP — macOS 主菜单本地化(日语) + * 创建时间: 2026-06-24 + * 作用: macOS 系统菜单栏日本語文案 + * ============================================================ */ + +/* ===== App Menu ===== */ +"1Xt-HY-uBw.title" = "閑言"; +"5kV-Vb-QxS.title" = "閑言について"; +"BOF-NM-1cW.title" = "設定…"; +"NMo-om-nkz.title" = "サービス"; +"Olw-nP-bQN.title" = "閑言を隠す"; +"Vdr-fp-XzO.title" = "ほかを隠す"; +"Kd2-mp-pUS.title" = "すべてを表示"; +"4sb-4s-VLi.title" = "閑言を終了"; + +/* ===== Edit Menu ===== */ +"5QF-Oa-p0T.title" = "編集"; +"dRJ-4n-Yzg.title" = "取り消す"; +"6dh-zS-Vam.title" = "やり直す"; +"uRl-iY-unG.title" = "カット"; +"x3v-GG-iWU.title" = "コピー"; +"gVA-U4-sdL.title" = "ペースト"; +"WeT-3V-zwk.title" = "スタイルを適用してペースト"; +"pa3-QI-u2k.title" = "削除"; +"Ruw-6m-B2m.title" = "すべてを選択"; + +/* ===== Edit > Find ===== */ +"4EN-yA-p0u.title" = "検索"; +"Xz5-n4-O0W.title" = "検索…"; +"YEy-JH-Tfz.title" = "検索と置換…"; +"q09-fT-Sye.title" = "次を検索"; +"OwM-mh-QMV.title" = "前を検索"; +"buJ-ug-pKt.title" = "選択部分を検索に使用"; +"S0p-oC-mLd.title" = "選択部分へジャンプ"; + +/* ===== Edit > Spelling and Grammar ===== */ +"Dv1-io-Yv7.title" = "スペルと文法"; +"HFo-cy-zxI.title" = "スペルと文法を表示"; +"hz2-CU-CR7.title" = "今すぐ書類をチェック"; +"rbD-Rh-wIN.title" = "入力中にスペルをチェック"; +"mK6-2p-4JG.title" = "スペルと一緒に文法をチェック"; +"78Y-hA-62v.title" = "スペルを自動的に修正"; + +/* ===== Edit > Substitutions ===== */ +"9ic-FL-obx.title" = "置換"; +"z6F-FW-3nz.title" = "置換を表示"; +"9yt-4B-nSM.title" = "スマートコピー/ペースト"; +"hQb-2v-fYv.title" = "スマート引用符"; +"rgM-f4-ycn.title" = "スマートダッシュ"; +"cwL-P1-jid.title" = "スマートリンク"; +"tRr-pd-1PS.title" = "データ検出"; +"HFQ-gK-NFA.title" = "テキスト置換"; + +/* ===== Edit > Transformations ===== */ +"2oI-Rn-ZJC.title" = "変換"; +"vmV-6d-7jI.title" = "大文字にする"; +"d9M-CD-aMd.title" = "小文字にする"; +"UEZ-Bs-lqG.title" = "頭文字を大文字にする"; + +/* ===== Edit > Speech ===== */ +"xrE-MZ-jX0.title" = "スピーチ"; +"Ynk-f8-cLZ.title" = "読み上げを開始"; +"Oyz-dy-DGm.title" = "読み上げを停止"; + +/* ===== View Menu ===== */ +"H8h-7b-M4v.title" = "表示"; + +/* ===== Window Menu ===== */ +"aUF-d1-5bR.title" = "ウインドウ"; +"OY7-WF-poV.title" = "しまう"; +"R4o-n2-Eq4.title" = "拡大/縮小"; +"LE2-aR-0XJ.title" = "すべてを手前に移動"; + +/* ===== Help Menu ===== */ +"EPT-qC-fAb.title" = "ヘルプ"; diff --git a/macos/Runner/ko.lproj/MainMenu.strings b/macos/Runner/ko.lproj/MainMenu.strings new file mode 100644 index 00000000..86ec1734 --- /dev/null +++ b/macos/Runner/ko.lproj/MainMenu.strings @@ -0,0 +1,76 @@ +/* ============================================================ + * 闲言APP — macOS 主菜单本地化(韩语) + * 创建时间: 2026-06-24 + * 作用: macOS 系统菜单栏한국어文案 + * ============================================================ */ + +/* ===== App Menu ===== */ +"1Xt-HY-uBw.title" = "한언"; +"5kV-Vb-QxS.title" = "한언 정보"; +"BOF-NM-1cW.title" = "환경설정…"; +"NMo-om-nkz.title" = "서비스"; +"Olw-nP-bQN.title" = "한언 숨기기"; +"Vdr-fp-XzO.title" = "기타 숨기기"; +"Kd2-mp-pUS.title" = "모두 보기"; +"4sb-4s-VLi.title" = "한언 종료"; + +/* ===== Edit Menu ===== */ +"5QF-Oa-p0T.title" = "편집"; +"dRJ-4n-Yzg.title" = "실행 취소"; +"6dh-zS-Vam.title" = "다시 실행"; +"uRl-iY-unG.title" = "잘라내기"; +"x3v-GG-iWU.title" = "복사"; +"gVA-U4-sdL.title" = "붙여넣기"; +"WeT-3V-zwk.title" = "스타일 적용하여 붙여넣기"; +"pa3-QI-u2k.title" = "삭제"; +"Ruw-6m-B2m.title" = "모두 선택"; + +/* ===== Edit > Find ===== */ +"4EN-yA-p0u.title" = "찾기"; +"Xz5-n4-O0W.title" = "찾기…"; +"YEy-JH-Tfz.title" = "찾기 및 바꾸기…"; +"q09-fT-Sye.title" = "다음 찾기"; +"OwM-mh-QMV.title" = "이전 찾기"; +"buJ-ug-pKt.title" = "선택 항목으로 찾기"; +"S0p-oC-mLd.title" = "선택 항목으로 이동"; + +/* ===== Edit > Spelling and Grammar ===== */ +"Dv1-io-Yv7.title" = "맞춤법 및 문법"; +"HFo-cy-zxI.title" = "맞춤법 및 문법 표시"; +"hz2-CU-CR7.title" = "지금 문서 검사"; +"rbD-Rh-wIN.title" = "입력하는 동안 맞춤법 검사"; +"mK6-2p-4JG.title" = "맞춤법과 함께 문법 검사"; +"78Y-hA-62v.title" = "맞춤법 자동 수정"; + +/* ===== Edit > Substitutions ===== */ +"9ic-FL-obx.title" = "대체"; +"z6F-FW-3nz.title" = "대체 표시"; +"9yt-4B-nSM.title" = "스마트 복사/붙여넣기"; +"hQb-2v-fYv.title" = "스마트 인용 부호"; +"rgM-f4-ycn.title" = "스마트 대시"; +"cwL-P1-jid.title" = "스마트 링크"; +"tRr-pd-1PS.title" = "데이터 감지기"; +"HFQ-gK-NFA.title" = "텍스트 대체"; + +/* ===== Edit > Transformations ===== */ +"2oI-Rn-ZJC.title" = "변환"; +"vmV-6d-7jI.title" = "대문자로 만들기"; +"d9M-CD-aMd.title" = "소문자로 만들기"; +"UEZ-Bs-lqG.title" = "첫 글자를 대문자로"; + +/* ===== Edit > Speech ===== */ +"xrE-MZ-jX0.title" = "음성"; +"Ynk-f8-cLZ.title" = "말하기 시작"; +"Oyz-dy-DGm.title" = "말하기 중지"; + +/* ===== View Menu ===== */ +"H8h-7b-M4v.title" = "보기"; + +/* ===== Window Menu ===== */ +"aUF-d1-5bR.title" = "윈도우"; +"OY7-WF-poV.title" = "최소화"; +"R4o-n2-Eq4.title" = "확대/축소"; +"LE2-aR-0XJ.title" = "모두 앞으로 가져오기"; + +/* ===== Help Menu ===== */ +"EPT-qC-fAb.title" = "도움말"; diff --git a/macos/Runner/zh-Hans.lproj/MainMenu.strings b/macos/Runner/zh-Hans.lproj/MainMenu.strings new file mode 100644 index 00000000..f3cd6b82 --- /dev/null +++ b/macos/Runner/zh-Hans.lproj/MainMenu.strings @@ -0,0 +1,76 @@ +/* ============================================================ + * 闲言APP — macOS 主菜单本地化(简体中文) + * 创建时间: 2026-06-24 + * 作用: macOS 系统菜单栏简体中文文案 + * ============================================================ */ + +/* ===== App Menu ===== */ +"1Xt-HY-uBw.title" = "闲言"; +"5kV-Vb-QxS.title" = "关于 闲言"; +"BOF-NM-1cW.title" = "偏好设置…"; +"NMo-om-nkz.title" = "服务"; +"Olw-nP-bQN.title" = "隐藏 闲言"; +"Vdr-fp-XzO.title" = "隐藏其他"; +"Kd2-mp-pUS.title" = "显示全部"; +"4sb-4s-VLi.title" = "退出 闲言"; + +/* ===== Edit Menu ===== */ +"5QF-Oa-p0T.title" = "编辑"; +"dRJ-4n-Yzg.title" = "撤销"; +"6dh-zS-Vam.title" = "重做"; +"uRl-iY-unG.title" = "剪切"; +"x3v-GG-iWU.title" = "复制"; +"gVA-U4-sdL.title" = "粘贴"; +"WeT-3V-zwk.title" = "粘贴并匹配样式"; +"pa3-QI-u2k.title" = "删除"; +"Ruw-6m-B2m.title" = "全选"; + +/* ===== Edit > Find ===== */ +"4EN-yA-p0u.title" = "查找"; +"Xz5-n4-O0W.title" = "查找…"; +"YEy-JH-Tfz.title" = "查找和替换…"; +"q09-fT-Sye.title" = "查找下一个"; +"OwM-mh-QMV.title" = "查找上一个"; +"buJ-ug-pKt.title" = "使用所选内容查找"; +"S0p-oC-mLd.title" = "跳到所选内容"; + +/* ===== Edit > Spelling and Grammar ===== */ +"Dv1-io-Yv7.title" = "拼写和语法"; +"HFo-cy-zxI.title" = "显示拼写和语法"; +"hz2-CU-CR7.title" = "立即检查文稿"; +"rbD-Rh-wIN.title" = "键入时检查拼写"; +"mK6-2p-4JG.title" = "随拼写检查语法"; +"78Y-hA-62v.title" = "自动纠正拼写"; + +/* ===== Edit > Substitutions ===== */ +"9ic-FL-obx.title" = "替换"; +"z6F-FW-3nz.title" = "显示替换"; +"9yt-4B-nSM.title" = "智能复制/粘贴"; +"hQb-2v-fYv.title" = "智能引号"; +"rgM-f4-ycn.title" = "智能破折号"; +"cwL-P1-jid.title" = "智能链接"; +"tRr-pd-1PS.title" = "数据检测器"; +"HFQ-gK-NFA.title" = "文本替换"; + +/* ===== Edit > Transformations ===== */ +"2oI-Rn-ZJC.title" = "变换"; +"vmV-6d-7jI.title" = "转为大写"; +"d9M-CD-aMd.title" = "转为小写"; +"UEZ-Bs-lqG.title" = "首字母大写"; + +/* ===== Edit > Speech ===== */ +"xrE-MZ-jX0.title" = "语音"; +"Ynk-f8-cLZ.title" = "开始朗读"; +"Oyz-dy-DGm.title" = "停止朗读"; + +/* ===== View Menu ===== */ +"H8h-7b-M4v.title" = "视图"; + +/* ===== Window Menu ===== */ +"aUF-d1-5bR.title" = "窗口"; +"OY7-WF-poV.title" = "最小化"; +"R4o-n2-Eq4.title" = "缩放"; +"LE2-aR-0XJ.title" = "前置全部窗口"; + +/* ===== Help Menu ===== */ +"EPT-qC-fAb.title" = "帮助"; diff --git a/macos/Runner/zh-Hant.lproj/MainMenu.strings b/macos/Runner/zh-Hant.lproj/MainMenu.strings new file mode 100644 index 00000000..97991ed5 --- /dev/null +++ b/macos/Runner/zh-Hant.lproj/MainMenu.strings @@ -0,0 +1,76 @@ +/* ============================================================ + * 闲言APP — macOS 主菜单本地化(繁体中文) + * 创建时间: 2026-06-24 + * 作用: macOS 系统菜单栏繁體中文文案 + * ============================================================ */ + +/* ===== App Menu ===== */ +"1Xt-HY-uBw.title" = "閒言"; +"5kV-Vb-QxS.title" = "關於 閒言"; +"BOF-NM-1cW.title" = "偏好設定…"; +"NMo-om-nkz.title" = "服務"; +"Olw-nP-bQN.title" = "隱藏 閒言"; +"Vdr-fp-XzO.title" = "隱藏其他"; +"Kd2-mp-pUS.title" = "顯示全部"; +"4sb-4s-VLi.title" = "結束 閒言"; + +/* ===== Edit Menu ===== */ +"5QF-Oa-p0T.title" = "編輯"; +"dRJ-4n-Yzg.title" = "還原"; +"6dh-zS-Vam.title" = "重做"; +"uRl-iY-unG.title" = "剪下"; +"x3v-GG-iWU.title" = "拷貝"; +"gVA-U4-sdL.title" = "貼上"; +"WeT-3V-zwk.title" = "貼上並符合樣式"; +"pa3-QI-u2k.title" = "刪除"; +"Ruw-6m-B2m.title" = "全選"; + +/* ===== Edit > Find ===== */ +"4EN-yA-p0u.title" = "尋找"; +"Xz5-n4-O0W.title" = "尋找…"; +"YEy-JH-Tfz.title" = "尋找與取代…"; +"q09-fT-Sye.title" = "尋找下一個"; +"OwM-mh-QMV.title" = "尋找上一個"; +"buJ-ug-pKt.title" = "使用選取範圍尋找"; +"S0p-oC-mLd.title" = "跳至選取範圍"; + +/* ===== Edit > Spelling and Grammar ===== */ +"Dv1-io-Yv7.title" = "拼字和文法"; +"HFo-cy-zxI.title" = "顯示拼字和文法"; +"hz2-CU-CR7.title" = "立即檢查文件"; +"rbD-Rh-wIN.title" = "鍵入時檢查拼字"; +"mK6-2p-4JG.title" = "隨拼字檢查文法"; +"78Y-hA-62v.title" = "自動校正拼字"; + +/* ===== Edit > Substitutions ===== */ +"9ic-FL-obx.title" = "取代"; +"z6F-FW-3nz.title" = "顯示取代"; +"9yt-4B-nSM.title" = "智慧拷貝/貼上"; +"hQb-2v-fYv.title" = "智慧引號"; +"rgM-f4-ycn.title" = "智慧破折號"; +"cwL-P1-jid.title" = "智慧連結"; +"tRr-pd-1PS.title" = "資料偵測器"; +"HFQ-gK-NFA.title" = "文字取代"; + +/* ===== Edit > Transformations ===== */ +"2oI-Rn-ZJC.title" = "變換"; +"vmV-6d-7jI.title" = "設為大寫"; +"d9M-CD-aMd.title" = "設為小寫"; +"UEZ-Bs-lqG.title" = "首字母大寫"; + +/* ===== Edit > Speech ===== */ +"xrE-MZ-jX0.title" = "語音"; +"Ynk-f8-cLZ.title" = "開始朗讀"; +"Oyz-dy-DGm.title" = "停止朗讀"; + +/* ===== View Menu ===== */ +"H8h-7b-M4v.title" = "顯示方式"; + +/* ===== Window Menu ===== */ +"aUF-d1-5bR.title" = "視窗"; +"OY7-WF-poV.title" = "縮到最小"; +"R4o-n2-Eq4.title" = "縮放"; +"LE2-aR-0XJ.title" = "將此程式所有視窗移至最前"; + +/* ===== Help Menu ===== */ +"EPT-qC-fAb.title" = "輔助說明"; diff --git a/pubspec.macos.yaml b/pubspec.macos.yaml index efa06ca5..06ece259 100644 --- a/pubspec.macos.yaml +++ b/pubspec.macos.yaml @@ -21,7 +21,7 @@ name: xianyan description: "闲言 — 灵感语录更纯粹。每日拾句 + 壁纸创作 APP" publish_to: 'none' -version: 6.6.25+2606241 +version: 6.6.25+2606260 # 年月日-次 7位 environment: diff --git a/pubspec.ohos.yaml b/pubspec.ohos.yaml index 00b25ff9..2a08edb6 100644 --- a/pubspec.ohos.yaml +++ b/pubspec.ohos.yaml @@ -20,7 +20,7 @@ name: xianyan description: "闲言 — 灵感语录更纯粹。每日拾句 + 壁纸创作 APP" publish_to: 'none' -version: 6.6.25+2606241 +version: 6.6.25+2606260 # 年月日-次 7位 environment: