diff --git a/.trae/documents/launcher-resx-i18n-plan.md b/.trae/documents/launcher-resx-i18n-plan.md new file mode 100644 index 0000000..ad3dcd7 --- /dev/null +++ b/.trae/documents/launcher-resx-i18n-plan.md @@ -0,0 +1,850 @@ +# 启动器 RESX 多语言适配实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 为 LanMountainDesktop.Launcher 引入 RESX 资源文件,实现启动器 UI 的多语言适配,消除所有硬编码中英文字符串。 + +**Architecture:** 在 Launcher 项目中创建 RESX 资源文件体系(默认 zh-CN + en-US/ja-JP/ko-KR),通过 .NET 内置 `ResourceManager` 机制实现本地化。启动时从主应用 `settings.json` 读取 `LanguageCode` 字段设置 `CultureInfo.CurrentUICulture`,AXAML 中使用 `x:Static` 引用资源,C# 代码中通过 `Strings.ResourceName` 强类型访问。 + +**Tech Stack:** .NET RESX 资源文件、Avalonia `x:Static` 标记扩展、`System.Globalization.CultureInfo` + +--- + +## 现状分析 + +### 问题概述 + +1. **启动器完全没有本地化支持**:所有 UI 字符串硬编码,中英文混杂严重 +2. **纯英文窗口**:SplashWindow、ErrorWindow、MultiInstancePromptWindow、DataLocationPromptWindow、LoadingDetailsWindow +3. **纯中文窗口**:OobeWindow、MigrationPromptWindow、UpdateWindow、ErrorDebugWindow、DevDebugWindow、PrivacyPolicyWindow +4. **启动器不读取主应用语言设置**:没有 `LanguageCode` 相关代码 +5. **硬编码字符串总量约 180+ 条**,分布在 11 个 AXAML 视图和 11 个 C# code-behind 文件中 + +### 方案选择:RESX vs JSON + +| 维度 | RESX(本方案) | JSON(主项目模式) | +|------|---------------|-------------------| +| 编译时安全 | ✅ 强类型 `Strings.KeyName` | ❌ 字符串键值 `L("key", "fallback")` | +| AXAML 集成 | ✅ `x:Static` 直接引用 | ❌ 需 code-behind 赋值 | +| 回退机制 | ✅ 内置(默认资源 → 特定文化) | ✅ 自定义 `fallback` 参数 | +| 新增语言 | 需添加 RESX 文件并重新编译 | 仅添加 JSON 文件 | +| AOT 兼容性 | ⚠️ 需额外配置 | ✅ 已验证 | +| 与主项目一致性 | ❌ 不同模式 | ✅ 一致 | + +**选择 RESX 的理由**:启动器是独立轻量进程,不需要运行时语言切换;强类型访问减少拼写错误;`x:Static` 比 code-behind 赋值更清晰;RESX 的内置回退机制足够满足启动器需求。 + +### AOT 兼容性说明 + +Launcher 项目支持 Native AOT 发布。RESX 的 `ResourceManager` 依赖反射,需要: +1. 在 `.csproj` 中添加 `` 确保资源不被修剪 +2. 在 AOT props 中添加 `TrimmerRootAssembly` 保留资源程序集 +3. 发布后进行 AOT 冒烟测试验证 + +--- + +## 文件结构规划 + +### 新增文件 + +| 文件 | 职责 | +|------|------| +| `Resources/Strings.resx` | 默认资源文件(zh-CN,回退资源) | +| `Resources/Strings.en-US.resx` | 英语资源 | +| `Resources/Strings.ja-JP.resx` | 日语资源 | +| `Resources/Strings.ko-KR.resx` | 韩语资源 | +| `Services/LanguagePreferenceService.cs` | 从 settings.json 读取 LanguageCode 并设置 CultureInfo | + +### 修改文件 + +| 文件 | 改动内容 | +|------|---------| +| `LanMountainDesktop.Launcher.csproj` | 添加 RESX 嵌入资源配置 | +| `LanMountainDesktop.Launcher.AOT.props` | 添加资源程序集修剪保留 | +| `Program.cs` | 启动时调用语言偏好初始化 | +| `Views/SplashWindow.axaml` | 替换硬编码字符串为 `x:Static` | +| `Views/SplashWindow.axaml.cs` | 替换 C# 硬编码字符串为 `Strings.XXX` | +| `Views/ErrorWindow.axaml` | 同上 | +| `Views/ErrorWindow.axaml.cs` | 同上 | +| `Views/MultiInstancePromptWindow.axaml` | 同上 | +| `Views/MultiInstancePromptWindow.axaml.cs` | 同上 | +| `Views/DataLocationPromptWindow.axaml` | 同上 | +| `Views/DataLocationPromptWindow.axaml.cs` | 同上 | +| `Views/LoadingDetailsWindow.axaml` | 同上 | +| `Views/LoadingDetailsWindow.axaml.cs` | 同上 | +| `Views/UpdateWindow.axaml` | 同上 | +| `Views/UpdateWindow.axaml.cs` | 同上 | +| `Views/ErrorDebugWindow.axaml` | 同上 | +| `Views/ErrorDebugWindow.axaml.cs` | 同上 | +| `Views/OobeWindow.axaml` | 同上 | +| `Views/OobeWindow.axaml.cs` | 同上 | +| `Views/MigrationPromptWindow.axaml` | 同上 | +| `Views/MigrationPromptWindow.axaml.cs` | 同上 | +| `Views/PrivacyPolicyWindow.axaml` | 同上 | +| `Views/PrivacyPolicyWindow.axaml.cs` | 同上 | +| `Views/DevDebugWindow.axaml` | 同上 | +| `Views/DevDebugWindow.axaml.cs` | 同上 | +| `Services/LauncherFlowCoordinator.cs` | 替换硬编码字符串 | +| `App.axaml.cs` | 替换预览模式硬编码字符串 | + +--- + +## RESX 键命名规范 + +采用 `ViewName_ElementDescription` 模式,PascalCase 分隔: + +- 窗口标题:`Splash_Title`、`Error_Title`、`MultiInstance_Title` +- 按钮文本:`Error_ButtonOpenLogs`、`Error_ButtonCopy`、`Error_ButtonRetry` +- 状态文本:`Splash_StatusInitializing`、`Loading_StatusPreparing` +- 描述文本:`DataLocation_DescSystemProfile`、`DataLocation_DescPortable` +- OOBE 步骤:`Oobe_StepWelcomeTitle`、`Oobe_StepAppearanceTitle` + +--- + +## 实施任务 + +### Task 1: 创建 RESX 基础设施 + +**Files:** +- Create: `LanMountainDesktop.Launcher/Resources/Strings.resx` +- Create: `LanMountainDesktop.Launcher/Resources/Strings.en-US.resx` +- Create: `LanMountainDesktop.Launcher/Resources/Strings.ja-JP.resx` +- Create: `LanMountainDesktop.Launcher/Resources/Strings.ko-KR.resx` +- Modify: `LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj` +- Modify: `LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.AOT.props` + +- [ ] **Step 1: 创建默认 RESX 文件(zh-CN 回退资源)** + +创建 `Resources/Strings.resx`,包含所有 180+ 条字符串的中文翻译。此文件同时作为回退资源和中文资源。 + +```xml + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms + System.Resources.ResXResourceWriter, System.Windows.Forms + + + 阑山桌面 + 阑山桌面 + 正在初始化... + [调试模式] 启动画面预览 + + + 阑山桌面 + 启动器无法确认启动状态 + 阑山桌面未达到预期的启动状态。 + 启动恢复 + 您可以检查日志、等待当前进程或激活正在运行的桌面实例。 + 诊断详情 + 打开日志 + 复制 + 等待 + 退出 + 重试 + 激活 + [调试] 启动器错误 + 启动器找不到桌面可执行文件 + 在调试模式下选择另一个可执行文件、检查日志,或在修复部署路径后重试。 + 检查日志后重试,等待上一次启动尝试完全结束。 + 检查日志或退出。旧进程仍在运行时,启动器不会创建新的桌面进程。 + 启动仍在进行中 + 桌面进程仍在运行,启动器不会启动第二个实例。 + + + 阑山桌面 + 阑山桌面已在运行 + 启动器检测到已存在的桌面实例,未启动新进程。 + 重复启动 + 您当前的设置为显示此提示而不自动打开桌面。 + 未创建第二个主进程。 + 复制 + 关闭 + 打开桌面 + 现有主进程 PID: {0}\nShell 状态: {1}\n未创建第二个主进程。 + + + 选择数据保存位置 + 选择数据保存位置 + 选择启动器和桌面数据的存储位置。您可以稍后在设置中更改。 + 应用目录不可写入 + 当前安装目录需要管理员权限才能写入。数据将存储在系统用户目录中。 + 保存在系统用户目录(推荐) + 数据与当前 Windows 用户绑定,在应用重新安装和更新后保持完整。 + 保存在应用安装目录(便携模式) + 适用于便携安装。整个应用文件夹可以连同数据一起移动到另一台机器。 + 取消 + 确认 + 检测到已有的系统数据。选择便携模式将自动迁移当前数据。 + + + 阑山桌面 - 加载详情 + 正在启动阑山桌面 + 正在初始化... + 正在准备组件 + 加载项目 + 完成 + 加载时发生错误。 + 详情 + 取消 + 准备就绪 + 正在加载插件... + 正在加载组件... + 正在加载资源... + 正在加载数据... + 正在下载... + 正在处理... + 完成 + 插件 + 组件 + 资源 + 数据 + 网络 + 设置 + 系统 + 其他 + + + 阑山桌面 - 更新 + 阑山桌面 + 更新 + 正在更新,请稍候... + 更新完成 + 更新失败 + 更新过程中发生错误 + [调试模式] 更新页面 + 预览更新进度界面 + + + 调试模式 + 调试设置 + 开发模式 + 启用后自动扫描开发目录 + + + 应用路径 + 未选择 + 浏览... + 此功能仅供开发人员使用 + 取消 + 确定 + 选择阑山桌面主程序可执行文件 + + + 欢迎使用阑山桌面 + 欢迎使用阑山桌面 + 你的桌面,不止一面 + 开始使用 + 个性化你的桌面 + 选择你喜欢的主题样式,可随时在设置中更改 + 外观模式 + 浅色模式 + 深色模式 + 主题色 + 莫奈取色来源 + 从桌面壁纸取色 + 自定义图片取色 + 不使用莫奈取色 + 选择数据保存位置 + 保存在系统用户目录(推荐) + 数据与当前 Windows 用户绑定,在应用重新安装和更新后保持完整。 + 保存在应用安装目录(便携模式) + 适用于便携安装。整个应用文件夹可以连同数据一起移动到另一台机器。 + 无法保存到应用目录 + 当前安装目录需要管理员权限才能写入。数据将存储在系统用户目录中。 + 启动与展示 + 在任务栏显示主桌面窗口 + 以滑动方式显示主窗口 + 启动时使用淡入过渡 + 融合桌面与弹入手势 + 登录 Windows 时自动启动阑山桌面 + 信息与隐私 + 发送匿名崩溃报告 + 发送匿名使用统计 + 隐私追踪 ID + 同意 + 《阑山桌面遥测隐私数据收集协议》 + 返回 + 下一步 + 欢迎使用阑山桌面 + 你的桌面,不止一面 + + + 阑山桌面 - 版本迁移 + 检测到旧版本 + 检测到您的系统中安装了旧版本的阑山桌面(0.8.4)... + 版本: + 位置: + 类型: + 安装版 + 卸载旧版本不会影响新版本的使用,您的个人数据将保留。 + 查看位置 + 暂不处理 + 卸载旧版本 + + + 阑山桌面遥测隐私数据收集协议 + 阑山桌面遥测隐私数据收集协议 + 请仔细阅读以下协议内容,了解我们如何收集、使用和保护您的数据 + 关闭 + + + 开发调试窗口 + 启动画面 + 错误页面 + 更新页面 + OOBE页面 + 数据位置选择 + 启用功能 + 打开 + 全部设为查看模式 + 全部设为功能模式 + 关闭 + + + 设备较慢,仍在启动,请稍候。 + 桌面主进程仍在运行,Launcher 会继续等待,不会重复启动。 + + + 正在初始化... + 正在检查更新... + 正在检查插件... + 正在启动主程序... + 准备就绪 + [预览] 这是启动器错误窗口预览。 + 正在处理 {0}... + 正在连接到活跃的启动器... + +``` + +- [ ] **Step 2: 创建 en-US RESX 文件** + +创建 `Resources/Strings.en-US.resx`,包含所有字符串的英文翻译。结构与默认文件相同,仅 `` 内容为英文。 + +```xml + +LanMountain Desktop +LanMountain Desktop +Initializing... +Launcher could not confirm startup +LanMountain Desktop did not reach the expected startup state. + +``` + +- [ ] **Step 3: 创建 ja-JP RESX 文件** + +创建 `Resources/Strings.ja-JP.resx`,包含所有字符串的日语翻译。 + +- [ ] **Step 4: 创建 ko-KR RESX 文件** + +创建 `Resources/Strings.ko-KR.resx`,包含所有字符串的韩语翻译。 + +- [ ] **Step 5: 修改 .csproj 添加 RESX 配置** + +在 `LanMountainDesktop.Launcher.csproj` 的 `` 中添加: + +```xml + + + PublicResXFileCodeGenerator + Strings.Designer.cs + + +``` + +注意:使用 `PublicResXFileCodeGenerator` 而非 `ResXFileCodeGenerator`,生成 `public` 类以便 AXAML 的 `x:Static` 可以访问。 + +- [ ] **Step 6: 修改 AOT props 添加资源程序集保留** + +在 `LanMountainDesktop.Launcher.AOT.props` 的 AOT 修剪配置 `` 中添加: + +```xml + +``` + +- [ ] **Step 7: 运行构建验证 RESX 生成** + +Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` +Expected: 构建成功,`Resources/Strings.Designer.cs` 自动生成 + +--- + +### Task 2: 创建语言偏好服务 + +**Files:** +- Create: `LanMountainDesktop.Launcher/Services/LanguagePreferenceService.cs` +- Modify: `LanMountainDesktop.Launcher/Program.cs` + +- [ ] **Step 1: 创建 LanguagePreferenceService** + +```csharp +using System.Globalization; +using System.Text.Json.Nodes; + +namespace LanMountainDesktop.Launcher.Services; + +internal static class LanguagePreferenceService +{ + public static string ResolveLanguageCode(string appRoot) + { + try + { + var dataLocationResolver = new DataLocationResolver(appRoot); + var settingsPath = HostAppSettingsOobeMerger.GetSettingsFilePath(dataLocationResolver.ResolveDataRoot()); + if (!File.Exists(settingsPath)) + { + return "zh-CN"; + } + + var root = JsonNode.Parse(File.ReadAllText(settingsPath))?.AsObject(); + if (root is not null && + root.TryGetPropertyValue("LanguageCode", out var node) && + node is JsonValue value && + value.TryGetValue(out var code) && + !string.IsNullOrWhiteSpace(code)) + { + return NormalizeLanguageCode(code); + } + } + catch + { + } + + return "zh-CN"; + } + + public static void ApplyLanguage(string languageCode) + { + var normalized = NormalizeLanguageCode(languageCode); + var culture = CultureInfo.GetCultureInfo(normalized); + CultureInfo.DefaultThreadCurrentCulture = culture; + CultureInfo.DefaultThreadCurrentUICulture = culture; + Thread.CurrentThread.CurrentCulture = culture; + Thread.CurrentThread.CurrentUICulture = culture; + } + + private static string NormalizeLanguageCode(string code) + { + return code.ToLowerInvariant() switch + { + "en-us" or "en" => "en-US", + "ja-jp" or "ja" => "ja-JP", + "ko-kr" or "ko" => "ko-KR", + _ => "zh-CN" + }; + } +} +``` + +- [ ] **Step 2: 在 Program.cs 中调用语言初始化** + +在 `Program.Main` 方法中,`BuildAvaloniaApp().StartWithClassicDesktopLifetime(args)` 之前添加语言初始化: + +```csharp +var appRoot = Commands.ResolveAppRoot(commandContext); +var languageCode = LanguagePreferenceService.ResolveLanguageCode(appRoot); +LanguagePreferenceService.ApplyLanguage(languageCode); +``` + +- [ ] **Step 3: 构建验证** + +Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` +Expected: 构建成功 + +--- + +### Task 3: 替换 SplashWindow 硬编码字符串 + +**Files:** +- Modify: `LanMountainDesktop.Launcher/Views/SplashWindow.axaml` +- Modify: `LanMountainDesktop.Launcher/Views/SplashWindow.axaml.cs` + +- [ ] **Step 1: 在 SplashWindow.axaml 中添加 RESX 命名空间并替换字符串** + +在 `` 标签添加命名空间: +```xml +xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources" +``` + +替换硬编码字符串: +- `Title="LanMountain Desktop"` → `Title="{x:Static res:Strings.Splash_Title}"` +- `Text="LanMountain Desktop"` (AppNameText) → `Text="{x:Static res:Strings.Splash_AppName}"` +- `Text="Initializing..."` (StatusText) → `Text="{x:Static res:Strings.Splash_StatusInitializing}"` + +注意:`VersionText` 的 `Text="0.0.0-dev (Administrate)"` 是动态设置的占位文本,保留原样(由 code-behind `SetVersionInfo` 方法设置)。 + +- [ ] **Step 2: 在 SplashWindow.axaml.cs 中替换 C# 硬编码字符串** + +将 `"[Debug Mode] Splash Preview"` 替换为 `Strings.Splash_DebugPreview`。 + +- [ ] **Step 3: 构建验证** + +Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` +Expected: 构建成功 + +--- + +### Task 4: 替换 ErrorWindow 硬编码字符串 + +**Files:** +- Modify: `LanMountainDesktop.Launcher/Views/ErrorWindow.axaml` +- Modify: `LanMountainDesktop.Launcher/Views/ErrorWindow.axaml.cs` + +- [ ] **Step 1: 在 ErrorWindow.axaml 中添加 RESX 命名空间并替换字符串** + +添加命名空间 `xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources"` + +AXAML 替换: +- `Title="LanMountain Desktop"` → `Title="{x:Static res:Strings.Error_Title}"` +- `Text="Launcher could not confirm startup"` → `Text="{x:Static res:Strings.Error_TitleCannotConfirm}"` +- `Text="LanMountain Desktop did not reach..."` → `Text="{x:Static res:Strings.Error_MessageNotReached}"` +- `Title="Startup recovery"` → `Title="{x:Static res:Strings.Error_SuggestionTitle}"` +- `Message="You can inspect logs..."` → `Message="{x:Static res:Strings.Error_SuggestionMessage}"` +- `Header="Diagnostic details"` → `Header="{x:Static res:Strings.Error_DiagnosticHeader}"` +- `Text="Open Logs"` → `Text="{x:Static res:Strings.Error_ButtonOpenLogs}"` +- `Text="Copy"` → `Text="{x:Static res:Strings.Error_ButtonCopy}"` +- `Content="Wait"` → `Content="{x:Static res:Strings.Error_ButtonWait}"` +- `Text="Exit"` → `Text="{x:Static res:Strings.Error_ButtonExit}"` +- `Content="Retry"` → `Content="{x:Static res:Strings.Error_ButtonRetry}"` + +- [ ] **Step 2: 在 ErrorWindow.axaml.cs 中替换 C# 硬编码字符串** + +将所有硬编码字符串替换为 `Strings.XXX` 调用: +- `"LanMountain Desktop did not reach..."` → `Strings.Error_MessageNotReached` +- `"[Debug] Launcher error"` → `Strings.Error_DebugTitle` +- `"Launcher could not find the desktop executable"` → `Strings.Error_HostNotFoundTitle` +- `"Pick another executable..."` → `Strings.Error_HostNotFoundMessage` +- `"Launcher could not confirm startup"` → `Strings.Error_TitleCannotConfirm` +- `"Inspect logs, then retry..."` → `Strings.Error_GenericMessage` +- `"Inspect logs or exit..."` → `Strings.Error_RunningHostMessage` +- `"Retry"` → `Strings.Error_ButtonRetry` +- `"Activate"` → `Strings.Error_ButtonActivate` +- `"Wait"` → `Strings.Error_ButtonWait` +- `"Startup is still pending"` → `Strings.Error_PendingTitle` +- `"The desktop process is still running..."` → `Strings.Error_PendingMessage` + +- [ ] **Step 3: 构建验证** + +Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` +Expected: 构建成功 + +--- + +### Task 5: 替换 MultiInstancePromptWindow 硬编码字符串 + +**Files:** +- Modify: `LanMountainDesktop.Launcher/Views/MultiInstancePromptWindow.axaml` +- Modify: `LanMountainDesktop.Launcher/Views/MultiInstancePromptWindow.axaml.cs` + +- [ ] **Step 1: 在 MultiInstancePromptWindow.axaml 中替换字符串** + +添加命名空间,替换: +- `Title="LanMountain Desktop"` → `Title="{x:Static res:Strings.MultiInstance_Title}"` +- `Text="LanMountain Desktop is already running"` → `Text="{x:Static res:Strings.MultiInstance_AlreadyRunning}"` +- `Text="Launcher found an existing..."` → `Text="{x:Static res:Strings.MultiInstance_AlreadyRunningMessage}"` +- `Title="Repeated launch"` → `Title="{x:Static res:Strings.MultiInstance_RepeatedLaunchTitle}"` +- `Message="Your current setting..."` → `Message="{x:Static res:Strings.MultiInstance_RepeatedLaunchMessage}"` +- `Text="No second Host process..."` → `Text="{x:Static res:Strings.MultiInstance_NoSecondProcess}"` +- `Text="Copy"` → `Text="{x:Static res:Strings.MultiInstance_ButtonCopy}"` +- `Text="Close"` → `Text="{x:Static res:Strings.MultiInstance_ButtonClose}"` +- `Text="Open desktop"` → `Text="{x:Static res:Strings.MultiInstance_ButtonOpenDesktop}"` + +- [ ] **Step 2: 在 MultiInstancePromptWindow.axaml.cs 中替换 C# 硬编码字符串** + +将格式化字符串替换为 `string.Format(Strings.MultiInstance_DetailsFormat, processId, shellState)` 等。 + +- [ ] **Step 3: 构建验证** + +Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` +Expected: 构建成功 + +--- + +### Task 6: 替换 DataLocationPromptWindow 硬编码字符串 + +**Files:** +- Modify: `LanMountainDesktop.Launcher/Views/DataLocationPromptWindow.axaml` +- Modify: `LanMountainDesktop.Launcher/Views/DataLocationPromptWindow.axaml.cs` + +- [ ] **Step 1: 在 DataLocationPromptWindow.axaml 中替换字符串** + +替换所有 12 个硬编码字符串为 `x:Static` 引用。 + +- [ ] **Step 2: 在 DataLocationPromptWindow.axaml.cs 中替换 C# 硬编码字符串** + +将 `"Existing system data was detected..."` 替换为 `Strings.DataLocation_MigrateWarning`。 + +- [ ] **Step 3: 构建验证** + +Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` +Expected: 构建成功 + +--- + +### Task 7: 替换 LoadingDetailsWindow 硬编码字符串 + +**Files:** +- Modify: `LanMountainDesktop.Launcher/Views/LoadingDetailsWindow.axaml` +- Modify: `LanMountainDesktop.Launcher/Views/LoadingDetailsWindow.axaml.cs` + +- [ ] **Step 1: 在 LoadingDetailsWindow.axaml 中替换字符串** + +替换所有硬编码字符串为 `x:Static` 引用。 + +- [ ] **Step 2: 在 LoadingDetailsWindow.axaml.cs 中替换 C# 硬编码字符串** + +替换 `GetStageDescription`、`GetItemDescription`、`GetTypeLabel` 方法中的硬编码字符串为 `Strings.XXX` 调用。 + +- [ ] **Step 3: 构建验证** + +Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` +Expected: 构建成功 + +--- + +### Task 8: 替换 UpdateWindow 硬编码字符串 + +**Files:** +- Modify: `LanMountainDesktop.Launcher/Views/UpdateWindow.axaml` +- Modify: `LanMountainDesktop.Launcher/Views/UpdateWindow.axaml.cs` + +- [ ] **Step 1: 在 UpdateWindow.axaml 中替换字符串** + +替换 `"Update"` 为 `x:Static res:Strings.Update_StatusUpdate`。 + +- [ ] **Step 2: 在 UpdateWindow.axaml.cs 中替换 C# 硬编码字符串** + +替换 `"更新完成"`、`"更新失败"`、`"更新过程中发生错误"`、`"[调试模式] 更新页面"`、`"预览更新进度界面"` 为 `Strings.XXX` 调用。 + +- [ ] **Step 3: 构建验证** + +Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` +Expected: 构建成功 + +--- + +### Task 9: 替换 ErrorDebugWindow 硬编码字符串 + +**Files:** +- Modify: `LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml` +- Modify: `LanMountainDesktop.Launcher/Views/ErrorDebugWindow.axaml.cs` + +- [ ] **Step 1: 在 ErrorDebugWindow.axaml 中替换字符串** + +该窗口已使用中文,替换所有硬编码中文字符串为 `x:Static` 引用。 + +- [ ] **Step 2: 在 ErrorDebugWindow.axaml.cs 中替换 C# 硬编码字符串** + +替换 `"Select LanMountainDesktop host executable"` 和 `"Not selected"` 为 `Strings.DebugDebug_SelectExeDialog` 和 `Strings.DebugDebug_NotSelected`。 + +- [ ] **Step 3: 构建验证** + +Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` +Expected: 构建成功 + +--- + +### Task 10: 替换 OobeWindow 硬编码字符串 + +**Files:** +- Modify: `LanMountainDesktop.Launcher/Views/OobeWindow.axaml` +- Modify: `LanMountainDesktop.Launcher/Views/OobeWindow.axaml.cs` + +这是最大的单个任务,OobeWindow 有约 42 个硬编码字符串。 + +- [ ] **Step 1: 在 OobeWindow.axaml 中替换字符串** + +添加命名空间,逐个替换所有硬编码中文字符串为 `x:Static` 引用。包括: +- 窗口标题、欢迎页文本 +- 外观设置页文本 +- 数据位置页文本 +- 启动展示页文本 +- 隐私页文本 +- 完成页文本 +- 导航按钮文本 + +- [ ] **Step 2: 在 OobeWindow.axaml.cs 中替换 C# 硬编码字符串(如有)** + +检查 code-behind 中是否有动态设置的硬编码字符串并替换。 + +- [ ] **Step 3: 构建验证** + +Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` +Expected: 构建成功 + +--- + +### Task 11: 替换 MigrationPromptWindow 硬编码字符串 + +**Files:** +- Modify: `LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml` +- Modify: `LanMountainDesktop.Launcher/Views/MigrationPromptWindow.axaml.cs` + +- [ ] **Step 1: 在 MigrationPromptWindow.axaml 中替换字符串** + +替换所有硬编码中文字符串为 `x:Static` 引用。 + +- [ ] **Step 2: 在 MigrationPromptWindow.axaml.cs 中替换 C# 硬编码字符串(如有)** + +- [ ] **Step 3: 构建验证** + +Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` +Expected: 构建成功 + +--- + +### Task 12: 替换 PrivacyPolicyWindow 硬编码字符串 + +**Files:** +- Modify: `LanMountainDesktop.Launcher/Views/PrivacyPolicyWindow.axaml` +- Modify: `LanMountainDesktop.Launcher/Views/PrivacyPolicyWindow.axaml.cs` + +- [ ] **Step 1: 在 PrivacyPolicyWindow.axaml 中替换字符串** + +替换标题、描述、关闭按钮等硬编码字符串。 + +- [ ] **Step 2: 在 PrivacyPolicyWindow.axaml.cs 中处理隐私政策正文** + +隐私政策正文(约 80 行 Markdown)目前硬编码在 C# 中。考虑: +- 方案 A:将 Markdown 正文也放入 RESX(支持多语言隐私政策) +- 方案 B:保留 Markdown 正文在 C# 中,仅替换窗口标题和按钮 + +推荐方案 A,将隐私政策 Markdown 正文放入 RESX 的 `Privacy_PolicyContent` 键中。 + +- [ ] **Step 3: 构建验证** + +Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` +Expected: 构建成功 + +--- + +### Task 13: 替换 DevDebugWindow 硬编码字符串 + +**Files:** +- Modify: `LanMountainDesktop.Launcher/Views/DevDebugWindow.axaml` +- Modify: `LanMountainDesktop.Launcher/Views/DevDebugWindow.axaml.cs` + +- [ ] **Step 1: 在 DevDebugWindow.axaml 中替换字符串** + +替换所有硬编码中文字符串为 `x:Static` 引用。 + +- [ ] **Step 2: 在 DevDebugWindow.axaml.cs 中替换 C# 硬编码字符串(如有)** + +- [ ] **Step 3: 构建验证** + +Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` +Expected: 构建成功 + +--- + +### Task 14: 替换 LauncherFlowCoordinator 和 App.axaml.cs 硬编码字符串 + +**Files:** +- Modify: `LanMountainDesktop.Launcher/Services/LauncherFlowCoordinator.cs` +- Modify: `LanMountainDesktop.Launcher/App.axaml.cs` + +- [ ] **Step 1: 在 LauncherFlowCoordinator.cs 中替换字符串** + +替换: +- `"设备较慢,仍在启动,请稍候。"` → `Strings.Coordinator_SlowDeviceMessage` +- `"桌面主进程仍在运行..."` → `Strings.Coordinator_RunningHostMessage` + +- [ ] **Step 2: 在 App.axaml.cs 中替换预览模式字符串** + +替换 `SimulateSplashPreviewAsync` 中的硬编码消息数组: +```csharp +var messages = new[] { Strings.Preview_SplashInitializing, Strings.Preview_SplashCheckingUpdates, Strings.Preview_SplashCheckingPlugins, Strings.Preview_SplashLaunchingHost, Strings.Preview_SplashReady }; +``` + +替换 `HandlePreviewCommand` 中的 `"[Preview] This is the launcher error window preview."` → `Strings.Preview_ErrorMessage` + +替换 `RunApplyUpdateWithWindowAsync` 中的硬编码字符串: +- `"Verifying update..."` → 使用 RESX 键 +- `"Applying plugin upgrades..."` → 使用 RESX 键 +- `"Cleaning up old deployments..."` → 使用 RESX 键 + +替换 `SimulateUpdatePreviewAsync` 中的 `$"Processing {stages[i]}..."` → `string.Format(Strings.Preview_UpdateProcessing, stages[i])` + +替换 `AttachToExistingCoordinatorAsync` 中的 `"Connecting to the active launcher..."` → `Strings.Preview_ActivationConnecting` + +- [ ] **Step 3: 构建验证** + +Run: `dotnet build LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Debug` +Expected: 构建成功 + +--- + +### Task 15: 完整构建和运行验证 + +**Files:** 无新增/修改 + +- [ ] **Step 1: 完整解决方案构建** + +Run: `dotnet build LanMountainDesktop.slnx -c Debug` +Expected: 构建成功,无错误 + +- [ ] **Step 2: 运行启动器预览命令验证中文** + +Run: `dotnet run --project LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -- preview-splash` +Expected: 启动画面显示中文 + +- [ ] **Step 3: 验证英文模式** + +临时将 `LanguagePreferenceService.ResolveLanguageCode` 返回 `"en-US"` 后运行预览命令,验证英文显示。 + +- [ ] **Step 4: 运行测试** + +Run: `dotnet test LanMountainDesktop.slnx -c Debug` +Expected: 所有测试通过 + +--- + +### Task 16: AOT 发布冒烟测试 + +**Files:** 无新增/修改 + +- [ ] **Step 1: AOT 发布测试** + +Run: `dotnet publish LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj -c Release -r win-x64 /p:PublishAot=true` +Expected: 发布成功 + +- [ ] **Step 2: 运行 AOT 发布产物验证** + +运行发布后的可执行文件,验证 RESX 资源正确加载。 + +--- + +## 实施顺序建议 + +1. **Task 1** (RESX 基础设施) → **Task 2** (语言偏好服务) — 必须首先完成 +2. **Task 3-9** (英文窗口) — 优先处理,解决用户提出的"只有英文"问题 +3. **Task 10-13** (中文窗口) — 次优先,完成完整 i18n 覆盖 +4. **Task 14** (服务层和 App) — 与 Task 3-13 并行或随后 +5. **Task 15-16** (验证) — 最后执行 + +## 风险与注意事项 + +1. **AOT 兼容性**:`ResourceManager` 在 Native AOT 下可能需要额外配置。如果 AOT 发布失败,需要添加 `DynamicDependency` 属性或使用 `System.Resources.Extensions` 包的源生成器。 +2. **OOBE 首次运行**:OOBE 在首次运行时 `settings.json` 不存在,此时 `LanguagePreferenceService` 会回退到 `zh-CN`。这是合理的行为。 +3. **`x:Static` 与 Avalonia CompiledBindings**:项目启用了 `AvaloniaUseCompiledBindingsByDefault`,需要确认 `x:Static` 在编译绑定模式下正常工作。如有问题,可在特定 AXAML 文件中添加 `x:CompileBindings="False"`。 +4. **RESX Designer.cs 生成**:确保 `.csproj` 中使用 `PublicResXFileCodeGenerator` 生成 `public` 类,否则 `x:Static` 无法访问。 +5. **隐私政策多语言**:隐私政策 Markdown 正文较长,放入 RESX 可能影响可读性。可考虑保留在 C# 中或使用独立资源文件。 diff --git a/.trae/specs/air-app-window-chrome/checklist.md b/.trae/specs/air-app-window-chrome/checklist.md index 15f1249..c3f30dd 100644 --- a/.trae/specs/air-app-window-chrome/checklist.md +++ b/.trae/specs/air-app-window-chrome/checklist.md @@ -1,7 +1,7 @@ # Checklist - [x] Descriptor supports Standard, Borderless, FullScreen, Tool, and BackgroundOnly modes. -- [x] World Clock Air APP keeps the LanMountain custom title bar. +- [x] World Clock Air APP uses FluentAvalonia standard title-bar chrome. - [x] Whiteboard Air APP opens as a fullscreen titlebar-less window. - [x] Air APP windows do not use fused desktop bottom-most services. - [x] Air APP windows do not use `Topmost=true` promotion. diff --git a/.trae/specs/air-app-window-chrome/spec.md b/.trae/specs/air-app-window-chrome/spec.md index f9cd7e9..83de3d8 100644 --- a/.trae/specs/air-app-window-chrome/spec.md +++ b/.trae/specs/air-app-window-chrome/spec.md @@ -8,10 +8,10 @@ Give Air APPs explicit window chrome modes so title bars, fullscreen windows, bo - Air APP host resolves an `AirAppWindowDescriptor` from launch options before creating content. - Supported chrome modes are `Standard`, `Borderless`, `FullScreen`, `Tool`, and `BackgroundOnly`. -- `Standard` uses the LanMountain custom title bar and normal app-window behavior. -- `Borderless` hides the custom title bar while keeping a normal app window. -- `FullScreen` hides the custom title bar, removes rounded shell chrome, and enters fullscreen. -- `Tool` keeps host-owned chrome but disables resizing and hides the taskbar entry. +- `Standard` uses FluentAvalonia `FAAppWindow` title-bar chrome and normal app-window behavior. +- `Borderless` removes title-bar chrome while keeping a normal app window surface. +- `FullScreen` removes title-bar chrome and enters fullscreen. +- `Tool` keeps FluentAvalonia title-bar chrome but disables resizing and hides the taskbar entry. - `BackgroundOnly` is reserved for a later background Air APP lifecycle and is not used by built-in v1 apps. - Built-in `world-clock` uses `Standard`; built-in `whiteboard` uses `FullScreen`. diff --git a/.trae/specs/air-app-window-chrome/tasks.md b/.trae/specs/air-app-window-chrome/tasks.md index 33bf37f..e10f857 100644 --- a/.trae/specs/air-app-window-chrome/tasks.md +++ b/.trae/specs/air-app-window-chrome/tasks.md @@ -5,3 +5,4 @@ - [x] Map built-in `whiteboard` to `FullScreen` chrome. - [x] Apply descriptor settings from `AirAppWindow`. - [x] Add regression tests for supported modes and built-in mode mapping. +- [x] Replace the hand-rolled Air APP title bar with FluentAvalonia `FAAppWindow` chrome. diff --git a/.trae/specs/clock-air-app-mvp/checklist.md b/.trae/specs/clock-air-app-mvp/checklist.md new file mode 100644 index 0000000..2ffd09d --- /dev/null +++ b/.trae/specs/clock-air-app-mvp/checklist.md @@ -0,0 +1,13 @@ +# Checklist + +- [x] Clicking `DesktopClock` and `DesktopWorldClock` opens the same global Clock Air APP type. +- [x] Repeated `world-clock` open requests use the global `world-clock:clock-suite:global` instance key. +- [x] Whiteboard Air APP keeps its per-component instance key behavior. +- [x] Clock Air APP opens as a normal application window, not a desktop-layer window. +- [x] Clock Air APP settings are independent from desktop clock widget settings. +- [x] Corrupt Clock Air APP settings fall back to defaults. +- [x] World clock time labels support 12-hour, 24-hour, and follow-system formatting. +- [x] Added localization keys are present in all four supported language files. +- [x] Build and automated tests pass. +- [ ] Manual visual verification in all four languages. +- [ ] Manual verification that minimizing keeps stopwatch and timer running while closing stops them. diff --git a/.trae/specs/clock-air-app-mvp/spec.md b/.trae/specs/clock-air-app-mvp/spec.md new file mode 100644 index 0000000..c20634d --- /dev/null +++ b/.trae/specs/clock-air-app-mvp/spec.md @@ -0,0 +1,42 @@ +# Clock Air APP MVP + +## Goal + +Upgrade the built-in `world-clock` Air APP into a focused clock suite while keeping desktop clock widgets as lightweight launch entry points. + +## Scope + +- Keep the existing Air APP id `world-clock` for Launcher lifecycle compatibility. +- Use one global Clock Air APP instance for every clock widget entry point. +- Provide four tabs: World Clock, Stopwatch, Timer, and Settings. +- Store Clock Air APP settings independently from desktop widget settings at `AirApps/Clock/settings.json`. +- Follow the host language setting and provide localized text for `zh-CN`, `en-US`, `ja-JP`, and `ko-KR`. + +## Behavior + +- `world-clock` opens as a standard resizable FluentAvalonia window. +- The default window size is approximately `780x560`, with a minimum of `680x480`. +- World Clock shows local time and a configurable city list. +- Default city list is Beijing, London, Sydney, and New York. +- Users can add, remove, and reorder city entries during the Air APP session; the list persists across restarts. +- Stopwatch supports start, pause, resume, lap, and reset; laps are kept in the current window session, up to 50 entries. +- Timer supports fixed presets, a custom minute duration, start, pause, resume, reset, and a completed state. +- Closing the Clock Air APP stops stopwatch and timer activity. +- Minimizing the window keeps stopwatch and timer activity running. +- Timer completion can activate the Clock Air APP window when the setting is enabled. + +## Settings + +- Time format: follow system, 24-hour, or 12-hour. +- Show seconds. +- Startup tab: last used tab, World Clock, Stopwatch, or Timer. +- Activate window when timer finishes. + +## Out of Scope + +- Desktop clock widget visual redesign. +- Alarms. +- Focus mode. +- System notifications. +- Running stopwatch or timer after the Air APP window is closed. +- Third-party plugin Air APP declarations. diff --git a/.trae/specs/clock-air-app-mvp/tasks.md b/.trae/specs/clock-air-app-mvp/tasks.md new file mode 100644 index 0000000..716f4f7 --- /dev/null +++ b/.trae/specs/clock-air-app-mvp/tasks.md @@ -0,0 +1,15 @@ +# Tasks + +- [x] Add Clock Air APP settings snapshot and JSON store. +- [x] Add shared Clock Air APP time formatting helpers. +- [x] Add stopwatch and timer state models with focused tests. +- [x] Replace the old world-clock view with `ClockAirAppView`. +- [x] Configure `world-clock` as a standard resizable Air APP window. +- [x] Make `world-clock` use a global single-instance key independent of source component id. +- [x] Add world clock city add, remove, and reorder behavior. +- [x] Add stopwatch tab with lap support. +- [x] Add timer tab with presets and custom duration. +- [x] Add independent Clock Air APP settings tab. +- [x] Add `zh-CN`, `en-US`, `ja-JP`, and `ko-KR` localization keys. +- [x] Ensure AirAppHost output includes localization JSON resources. +- [x] Add regression tests for Launcher keying, descriptors, settings, formatting, stopwatch, timer, and localization coverage. diff --git a/LanMountainDesktop.AirAppHost/AirAppWindow.axaml b/LanMountainDesktop.AirAppHost/AirAppWindow.axaml index e352015..6cba450 100644 --- a/LanMountainDesktop.AirAppHost/AirAppWindow.axaml +++ b/LanMountainDesktop.AirAppHost/AirAppWindow.axaml @@ -1,64 +1,16 @@ - - - - - - - - - - - - - - - - + + + + + diff --git a/LanMountainDesktop.AirAppHost/AirAppWindow.axaml.cs b/LanMountainDesktop.AirAppHost/AirAppWindow.axaml.cs index d811a28..6e3af58 100644 --- a/LanMountainDesktop.AirAppHost/AirAppWindow.axaml.cs +++ b/LanMountainDesktop.AirAppHost/AirAppWindow.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; +using Avalonia.Media; using Avalonia.Threading; +using FluentAvalonia.UI.Windowing; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Services; using LanMountainDesktop.Shared.IPC; @@ -10,7 +10,7 @@ using LanMountainDesktop.Views.Components; namespace LanMountainDesktop.AirAppHost; -public sealed partial class AirAppWindow : Window +public sealed partial class AirAppWindow : FAAppWindow { private readonly AirAppLaunchOptions _options; private readonly AirAppWindowDescriptor _descriptor; @@ -36,7 +36,7 @@ public sealed partial class AirAppWindow : Window if (string.Equals(_options.AppId, AirAppLaunchOptions.WorldClockAppId, StringComparison.OrdinalIgnoreCase)) { - ContentHost.Content = new WorldClockAirAppView(_options); + ContentHost.Content = new ClockAirAppView(_options); return; } @@ -56,41 +56,41 @@ public sealed partial class AirAppWindow : Window private void ApplyWindowDescriptor(AirAppWindowDescriptor descriptor) { Title = descriptor.Title; - TitleTextBlock.Text = descriptor.TitleText; - SubtitleTextBlock.Text = descriptor.SubtitleText; Width = descriptor.Width; Height = descriptor.Height; MinWidth = descriptor.MinWidth; MinHeight = descriptor.MinHeight; ShowInTaskbar = descriptor.ShowInTaskbar; CanResize = descriptor.CanResize; - WindowDecorations = WindowDecorations.None; - ExtendClientAreaToDecorationsHint = true; - ExtendClientAreaTitleBarHeightHint = -1; - - TitleBar.IsVisible = true; - Grid.SetRow(ContentHost, 1); - Grid.SetRowSpan(ContentHost, 1); + ShowAsDialog = descriptor.ShowAsDialog; WindowState = WindowState.Normal; + WindowRoot.Background = this.TryFindResource("AirAppWindowBackgroundBrush", out var brush) && brush is IBrush backgroundBrush + ? backgroundBrush + : Brushes.White; + ConfigureTitleBar(descriptor); switch (descriptor.ChromeMode) { case AirAppWindowChromeMode.Standard: + WindowDecorations = WindowDecorations.Full; + TitleBar.ExtendsContentIntoTitleBar = false; break; case AirAppWindowChromeMode.Borderless: - HideCustomTitleBar(); + WindowDecorations = WindowDecorations.None; + TitleBar.ExtendsContentIntoTitleBar = true; break; case AirAppWindowChromeMode.FullScreen: - HideCustomTitleBar(); - WindowShell.CornerRadius = new Avalonia.CornerRadius(0); - WindowShell.BorderThickness = new Avalonia.Thickness(0); - WindowShell.BoxShadow = default; + WindowDecorations = WindowDecorations.None; + TitleBar.ExtendsContentIntoTitleBar = true; + ShowAsDialog = false; WindowState = WindowState.FullScreen; break; case AirAppWindowChromeMode.Tool: + WindowDecorations = WindowDecorations.Full; + TitleBar.ExtendsContentIntoTitleBar = false; ShowInTaskbar = false; CanResize = false; break; @@ -102,11 +102,18 @@ public sealed partial class AirAppWindow : Window } } - private void HideCustomTitleBar() + private void ConfigureTitleBar(AirAppWindowDescriptor descriptor) { - TitleBar.IsVisible = false; - Grid.SetRow(ContentHost, 0); - Grid.SetRowSpan(ContentHost, 2); + TitleBar.Height = descriptor.ChromeMode == AirAppWindowChromeMode.Tool ? 36 : 40; + TitleBar.BackgroundColor = Colors.Transparent; + TitleBar.ForegroundColor = Color.FromRgb(32, 32, 32); + TitleBar.InactiveBackgroundColor = Colors.Transparent; + TitleBar.InactiveForegroundColor = Color.FromRgb(96, 96, 96); + TitleBar.ButtonBackgroundColor = Colors.Transparent; + TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(23, 0, 0, 0); + TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(52, 0, 0, 0); + TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent; + TitleBar.ButtonInactiveForegroundColor = Colors.Gray; } private void ConfigureWhiteboardWindow() @@ -147,6 +154,12 @@ public sealed partial class AirAppWindow : Window }, DispatcherPriority.Background); } + protected override void OnClosing(WindowClosingEventArgs e) + { + SaveWhiteboard(); + base.OnClosing(e); + } + protected override void OnClosed(EventArgs e) { SaveAndDisposeWhiteboard(); @@ -154,20 +167,6 @@ public sealed partial class AirAppWindow : Window base.OnClosed(e); } - private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) - { - BeginMoveDrag(e); - } - } - - private void OnCloseClick(object? sender, RoutedEventArgs e) - { - SaveWhiteboard(); - Close(); - } - private void SaveAndDisposeWhiteboard() { var widget = _whiteboardWidget; @@ -259,6 +258,11 @@ public sealed partial class AirAppWindow : Window return _options.InstanceKey.Trim(); } + if (string.Equals(_options.AppId, AirAppLaunchOptions.WorldClockAppId, StringComparison.OrdinalIgnoreCase)) + { + return $"{AirAppLaunchOptions.WorldClockAppId}:clock-suite:global"; + } + var componentId = string.IsNullOrWhiteSpace(_options.SourceComponentId) ? "none" : _options.SourceComponentId.Trim(); diff --git a/LanMountainDesktop.AirAppHost/AirAppWindowDescriptor.cs b/LanMountainDesktop.AirAppHost/AirAppWindowDescriptor.cs index 1178154..4a5111d 100644 --- a/LanMountainDesktop.AirAppHost/AirAppWindowDescriptor.cs +++ b/LanMountainDesktop.AirAppHost/AirAppWindowDescriptor.cs @@ -7,6 +7,7 @@ public sealed record AirAppWindowDescriptor( AirAppWindowChromeMode ChromeMode, bool CanResize, bool ShowInTaskbar, + bool ShowAsDialog, double Width, double Height, double MinWidth, @@ -23,13 +24,15 @@ public sealed record AirAppWindowDescriptor( if (string.Equals(options.AppId, AirAppLaunchOptions.WorldClockAppId, StringComparison.OrdinalIgnoreCase)) { return Standard( - "World Clock - Air APP", - "World Clock", + "Clock - Air APP", + "Clock", "Air APP", - width: 360, - height: 220, - minWidth: 320, - minHeight: 220); + width: 780, + height: 560, + minWidth: 680, + minHeight: 480, + canResize: true, + showAsDialog: false); } if (string.Equals(options.AppId, AirAppLaunchOptions.WhiteboardAppId, StringComparison.OrdinalIgnoreCase)) @@ -53,15 +56,18 @@ public sealed record AirAppWindowDescriptor( double width = 520, double height = 360, double minWidth = 360, - double minHeight = 260) + double minHeight = 260, + bool canResize = true, + bool showAsDialog = false) { return new AirAppWindowDescriptor( windowTitle, titleBarTitle, titleBarSubtitle, AirAppWindowChromeMode.Standard, - CanResize: true, + CanResize: canResize, ShowInTaskbar: true, + ShowAsDialog: showAsDialog, width, height, minWidth, @@ -80,6 +86,7 @@ public sealed record AirAppWindowDescriptor( AirAppWindowChromeMode.FullScreen, CanResize: false, ShowInTaskbar: true, + ShowAsDialog: false, Width: 1280, Height: 720, MinWidth: 360, @@ -98,6 +105,7 @@ public sealed record AirAppWindowDescriptor( AirAppWindowChromeMode.Borderless, CanResize: true, ShowInTaskbar: true, + ShowAsDialog: false, width, height, MinWidth: 240, @@ -118,6 +126,7 @@ public sealed record AirAppWindowDescriptor( AirAppWindowChromeMode.Tool, CanResize: false, ShowInTaskbar: false, + ShowAsDialog: true, width, height, MinWidth: 240, @@ -133,6 +142,7 @@ public sealed record AirAppWindowDescriptor( AirAppWindowChromeMode.BackgroundOnly, CanResize: false, ShowInTaskbar: false, + ShowAsDialog: false, Width: 1, Height: 1, MinWidth: 1, diff --git a/LanMountainDesktop.AirAppHost/ClockAirAppView.axaml b/LanMountainDesktop.AirAppHost/ClockAirAppView.axaml new file mode 100644 index 0000000..a246b95 --- /dev/null +++ b/LanMountainDesktop.AirAppHost/ClockAirAppView.axaml @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -117,7 +119,7 @@ Height="34"> - + @@ -126,7 +128,7 @@ Orientation="Horizontal" Spacing="8"> @@ -105,7 +107,7 @@ Height="34"> - + diff --git a/LanMountainDesktop.Launcher/Views/MultiInstancePromptWindow.axaml.cs b/LanMountainDesktop.Launcher/Views/MultiInstancePromptWindow.axaml.cs index 65c1d44..56d3081 100644 --- a/LanMountainDesktop.Launcher/Views/MultiInstancePromptWindow.axaml.cs +++ b/LanMountainDesktop.Launcher/Views/MultiInstancePromptWindow.axaml.cs @@ -2,6 +2,7 @@ using Avalonia.Controls; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; +using LanMountainDesktop.Launcher.Resources; namespace LanMountainDesktop.Launcher.Views; @@ -9,7 +10,7 @@ public partial class MultiInstancePromptWindow : Window { private readonly TaskCompletionSource _completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - private string _details = "LanMountain Desktop is already running."; + private string _details = Strings.MultiInstance_AlreadyRunning; public MultiInstancePromptWindow() { @@ -22,7 +23,7 @@ public partial class MultiInstancePromptWindow : Window public void SetDetails(int processId, string shellState) { - _details = $"Existing host PID: {processId}\nShell state: {shellState}\nNo second Host process was created."; + _details = string.Format(Strings.MultiInstance_DetailsFormat, processId, shellState); if (this.FindControl("DetailsText") is { } detailsText) { diff --git a/LanMountainDesktop.Launcher/Views/OobeWindow.axaml b/LanMountainDesktop.Launcher/Views/OobeWindow.axaml index e098665..a2fefe3 100644 --- a/LanMountainDesktop.Launcher/Views/OobeWindow.axaml +++ b/LanMountainDesktop.Launcher/Views/OobeWindow.axaml @@ -5,12 +5,14 @@ xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views" xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:fi="using:FluentIcons.Avalonia" + xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources" mc:Ignorable="d" d:DesignWidth="850" d:DesignHeight="650" x:Class="LanMountainDesktop.Launcher.Views.OobeWindow" x:DataType="views:OobeWindow" - Title="欢迎使用阑山桌面" + x:CompileBindings="False" + Title="{x:Static res:Strings.Oobe_Title}" Width="850" Height="650" CanResize="False" @@ -149,7 +151,7 @@ - @@ -173,11 +175,11 @@ - - @@ -189,7 +191,7 @@ CornerRadius="{DynamicResource DesignCornerRadiusMd}" Padding="16"> - @@ -215,7 +217,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center"/> - @@ -247,7 +249,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center"/> - @@ -265,7 +267,7 @@ CornerRadius="{DynamicResource DesignCornerRadiusMd}" Padding="16"> - @@ -335,11 +337,11 @@ CornerRadius="{DynamicResource DesignCornerRadiusMd}" Padding="16"> - - @@ -359,11 +361,11 @@ VerticalAlignment="Center" Margin="0,0,12,0" /> - - @@ -384,11 +386,11 @@ VerticalAlignment="Center" Margin="0,0,12,0" /> - - @@ -409,11 +411,11 @@ VerticalAlignment="Center" Margin="0,0,12,0" /> - - @@ -431,10 +433,10 @@ Spacing="12" Margin="0,24,0,0">