mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 15:44:25 +08:00
feat.完善了时钟轻应用,为启动器提供了多语言支持
This commit is contained in:
850
.trae/documents/launcher-resx-i18n-plan.md
Normal file
850
.trae/documents/launcher-resx-i18n-plan.md
Normal file
@@ -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` 中添加 `<EmbeddedResource>` 确保资源不被修剪
|
||||
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
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
<xsd:attribute name="type" type="xsd:string" use="optional" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" use="optional" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
|
||||
<resheader name="version"><value>2.0</value></resheader>
|
||||
<resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms</value></resheader>
|
||||
<resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms</value></resheader>
|
||||
|
||||
<!-- SplashWindow -->
|
||||
<data name="Splash_Title" xml:space="preserve"><value>阑山桌面</value></data>
|
||||
<data name="Splash_AppName" xml:space="preserve"><value>阑山桌面</value></data>
|
||||
<data name="Splash_StatusInitializing" xml:space="preserve"><value>正在初始化...</value></data>
|
||||
<data name="Splash_DebugPreview" xml:space="preserve"><value>[调试模式] 启动画面预览</value></data>
|
||||
|
||||
<!-- ErrorWindow -->
|
||||
<data name="Error_Title" xml:space="preserve"><value>阑山桌面</value></data>
|
||||
<data name="Error_TitleCannotConfirm" xml:space="preserve"><value>启动器无法确认启动状态</value></data>
|
||||
<data name="Error_MessageNotReached" xml:space="preserve"><value>阑山桌面未达到预期的启动状态。</value></data>
|
||||
<data name="Error_SuggestionTitle" xml:space="preserve"><value>启动恢复</value></data>
|
||||
<data name="Error_SuggestionMessage" xml:space="preserve"><value>您可以检查日志、等待当前进程或激活正在运行的桌面实例。</value></data>
|
||||
<data name="Error_DiagnosticHeader" xml:space="preserve"><value>诊断详情</value></data>
|
||||
<data name="Error_ButtonOpenLogs" xml:space="preserve"><value>打开日志</value></data>
|
||||
<data name="Error_ButtonCopy" xml:space="preserve"><value>复制</value></data>
|
||||
<data name="Error_ButtonWait" xml:space="preserve"><value>等待</value></data>
|
||||
<data name="Error_ButtonExit" xml:space="preserve"><value>退出</value></data>
|
||||
<data name="Error_ButtonRetry" xml:space="preserve"><value>重试</value></data>
|
||||
<data name="Error_ButtonActivate" xml:space="preserve"><value>激活</value></data>
|
||||
<data name="Error_DebugTitle" xml:space="preserve"><value>[调试] 启动器错误</value></data>
|
||||
<data name="Error_HostNotFoundTitle" xml:space="preserve"><value>启动器找不到桌面可执行文件</value></data>
|
||||
<data name="Error_HostNotFoundMessage" xml:space="preserve"><value>在调试模式下选择另一个可执行文件、检查日志,或在修复部署路径后重试。</value></data>
|
||||
<data name="Error_GenericMessage" xml:space="preserve"><value>检查日志后重试,等待上一次启动尝试完全结束。</value></data>
|
||||
<data name="Error_RunningHostMessage" xml:space="preserve"><value>检查日志或退出。旧进程仍在运行时,启动器不会创建新的桌面进程。</value></data>
|
||||
<data name="Error_PendingTitle" xml:space="preserve"><value>启动仍在进行中</value></data>
|
||||
<data name="Error_PendingMessage" xml:space="preserve"><value>桌面进程仍在运行,启动器不会启动第二个实例。</value></data>
|
||||
|
||||
<!-- MultiInstancePromptWindow -->
|
||||
<data name="MultiInstance_Title" xml:space="preserve"><value>阑山桌面</value></data>
|
||||
<data name="MultiInstance_AlreadyRunning" xml:space="preserve"><value>阑山桌面已在运行</value></data>
|
||||
<data name="MultiInstance_AlreadyRunningMessage" xml:space="preserve"><value>启动器检测到已存在的桌面实例,未启动新进程。</value></data>
|
||||
<data name="MultiInstance_RepeatedLaunchTitle" xml:space="preserve"><value>重复启动</value></data>
|
||||
<data name="MultiInstance_RepeatedLaunchMessage" xml:space="preserve"><value>您当前的设置为显示此提示而不自动打开桌面。</value></data>
|
||||
<data name="MultiInstance_NoSecondProcess" xml:space="preserve"><value>未创建第二个主进程。</value></data>
|
||||
<data name="MultiInstance_ButtonCopy" xml:space="preserve"><value>复制</value></data>
|
||||
<data name="MultiInstance_ButtonClose" xml:space="preserve"><value>关闭</value></data>
|
||||
<data name="MultiInstance_ButtonOpenDesktop" xml:space="preserve"><value>打开桌面</value></data>
|
||||
<data name="MultiInstance_DetailsFormat" xml:space="preserve"><value>现有主进程 PID: {0}\nShell 状态: {1}\n未创建第二个主进程。</value></data>
|
||||
|
||||
<!-- DataLocationPromptWindow -->
|
||||
<data name="DataLocation_Title" xml:space="preserve"><value>选择数据保存位置</value></data>
|
||||
<data name="DataLocation_ChooseLocation" xml:space="preserve"><value>选择数据保存位置</value></data>
|
||||
<data name="DataLocation_ChooseLocationDesc" xml:space="preserve"><value>选择启动器和桌面数据的存储位置。您可以稍后在设置中更改。</value></data>
|
||||
<data name="DataLocation_NotWritable" xml:space="preserve"><value>应用目录不可写入</value></data>
|
||||
<data name="DataLocation_NotWritableDesc" xml:space="preserve"><value>当前安装目录需要管理员权限才能写入。数据将存储在系统用户目录中。</value></data>
|
||||
<data name="DataLocation_SystemProfile" xml:space="preserve"><value>保存在系统用户目录(推荐)</value></data>
|
||||
<data name="DataLocation_SystemProfileDesc" xml:space="preserve"><value>数据与当前 Windows 用户绑定,在应用重新安装和更新后保持完整。</value></data>
|
||||
<data name="DataLocation_Portable" xml:space="preserve"><value>保存在应用安装目录(便携模式)</value></data>
|
||||
<data name="DataLocation_PortableDesc" xml:space="preserve"><value>适用于便携安装。整个应用文件夹可以连同数据一起移动到另一台机器。</value></data>
|
||||
<data name="DataLocation_ButtonCancel" xml:space="preserve"><value>取消</value></data>
|
||||
<data name="DataLocation_ButtonConfirm" xml:space="preserve"><value>确认</value></data>
|
||||
<data name="DataLocation_MigrateWarning" xml:space="preserve"><value>检测到已有的系统数据。选择便携模式将自动迁移当前数据。</value></data>
|
||||
|
||||
<!-- LoadingDetailsWindow -->
|
||||
<data name="Loading_Title" xml:space="preserve"><value>阑山桌面 - 加载详情</value></data>
|
||||
<data name="Loading_StartingDesktop" xml:space="preserve"><value>正在启动阑山桌面</value></data>
|
||||
<data name="Loading_StatusInitializing" xml:space="preserve"><value>正在初始化...</value></data>
|
||||
<data name="Loading_StatusPreparing" xml:space="preserve"><value>正在准备组件</value></data>
|
||||
<data name="Loading_LoadingItems" xml:space="preserve"><value>加载项目</value></data>
|
||||
<data name="Loading_Done" xml:space="preserve"><value>完成</value></data>
|
||||
<data name="Loading_ErrorOccurred" xml:space="preserve"><value>加载时发生错误。</value></data>
|
||||
<data name="Loading_ButtonDetails" xml:space="preserve"><value>详情</value></data>
|
||||
<data name="Loading_ButtonCancel" xml:space="preserve"><value>取消</value></data>
|
||||
<data name="Loading_StageReady" xml:space="preserve"><value>准备就绪</value></data>
|
||||
<data name="Loading_ItemPlugin" xml:space="preserve"><value>正在加载插件...</value></data>
|
||||
<data name="Loading_ItemComponent" xml:space="preserve"><value>正在加载组件...</value></data>
|
||||
<data name="Loading_ItemResource" xml:space="preserve"><value>正在加载资源...</value></data>
|
||||
<data name="Loading_ItemData" xml:space="preserve"><value>正在加载数据...</value></data>
|
||||
<data name="Loading_ItemDownload" xml:space="preserve"><value>正在下载...</value></data>
|
||||
<data name="Loading_ItemProcess" xml:space="preserve"><value>正在处理...</value></data>
|
||||
<data name="Loading_ItemComplete" xml:space="preserve"><value>完成</value></data>
|
||||
<data name="Loading_TypePlugin" xml:space="preserve"><value>插件</value></data>
|
||||
<data name="Loading_TypeComponent" xml:space="preserve"><value>组件</value></data>
|
||||
<data name="Loading_TypeResource" xml:space="preserve"><value>资源</value></data>
|
||||
<data name="Loading_TypeData" xml:space="preserve"><value>数据</value></data>
|
||||
<data name="Loading_TypeNetwork" xml:space="preserve"><value>网络</value></data>
|
||||
<data name="Loading_TypeSettings" xml:space="preserve"><value>设置</value></data>
|
||||
<data name="Loading_TypeSystem" xml:space="preserve"><value>系统</value></data>
|
||||
<data name="Loading_TypeOther" xml:space="preserve"><value>其他</value></data>
|
||||
|
||||
<!-- UpdateWindow -->
|
||||
<data name="Update_Title" xml:space="preserve"><value>阑山桌面 - 更新</value></data>
|
||||
<data name="Update_AppName" xml:space="preserve"><value>阑山桌面</value></data>
|
||||
<data name="Update_StatusUpdate" xml:space="preserve"><value>更新</value></data>
|
||||
<data name="Update_StatusUpdating" xml:space="preserve"><value>正在更新,请稍候...</value></data>
|
||||
<data name="Update_Complete" xml:space="preserve"><value>更新完成</value></data>
|
||||
<data name="Update_Failed" xml:space="preserve"><value>更新失败</value></data>
|
||||
<data name="Update_FailedMessage" xml:space="preserve"><value>更新过程中发生错误</value></data>
|
||||
<data name="Update_DebugTitle" xml:space="preserve"><value>[调试模式] 更新页面</value></data>
|
||||
<data name="Update_DebugMessage" xml:space="preserve"><value>预览更新进度界面</value></data>
|
||||
|
||||
<!-- ErrorDebugWindow -->
|
||||
<data name="DebugDebug_Title" xml:space="preserve"><value>调试模式</value></data>
|
||||
<data name="DebugDebug_SettingsTitle" xml:space="preserve"><value>调试设置</value></data>
|
||||
<data name="DebugDebug_DevMode" xml:space="preserve"><value>开发模式</value></data>
|
||||
<data name="DebugDebug_DevModeDesc" xml:space="preserve"><value>启用后自动扫描开发目录</value></data>
|
||||
<data name="DebugDebug_On" xml:space="preserve"><value>开</value></data>
|
||||
<data name="DebugDebug_Off" xml:space="preserve"><value>关</value></data>
|
||||
<data name="DebugDebug_AppPath" xml:space="preserve"><value>应用路径</value></data>
|
||||
<data name="DebugDebug_NotSelected" xml:space="preserve"><value>未选择</value></data>
|
||||
<data name="DebugDebug_Browse" xml:space="preserve"><value>浏览...</value></data>
|
||||
<data name="DebugDebug_Warning" xml:space="preserve"><value>此功能仅供开发人员使用</value></data>
|
||||
<data name="DebugDebug_ButtonCancel" xml:space="preserve"><value>取消</value></data>
|
||||
<data name="DebugDebug_ButtonOk" xml:space="preserve"><value>确定</value></data>
|
||||
<data name="DebugDebug_SelectExeDialog" xml:space="preserve"><value>选择阑山桌面主程序可执行文件</value></data>
|
||||
|
||||
<!-- OobeWindow -->
|
||||
<data name="Oobe_Title" xml:space="preserve"><value>欢迎使用阑山桌面</value></data>
|
||||
<data name="Oobe_WelcomeTitle" xml:space="preserve"><value>欢迎使用阑山桌面</value></data>
|
||||
<data name="Oobe_WelcomeSubtitle" xml:space="preserve"><value>你的桌面,不止一面</value></data>
|
||||
<data name="Oobe_ButtonGetStarted" xml:space="preserve"><value>开始使用</value></data>
|
||||
<data name="Oobe_AppearanceTitle" xml:space="preserve"><value>个性化你的桌面</value></data>
|
||||
<data name="Oobe_AppearanceDesc" xml:space="preserve"><value>选择你喜欢的主题样式,可随时在设置中更改</value></data>
|
||||
<data name="Oobe_AppearanceMode" xml:space="preserve"><value>外观模式</value></data>
|
||||
<data name="Oobe_LightMode" xml:space="preserve"><value>浅色模式</value></data>
|
||||
<data name="Oobe_DarkMode" xml:space="preserve"><value>深色模式</value></data>
|
||||
<data name="Oobe_ThemeColor" xml:space="preserve"><value>主题色</value></data>
|
||||
<data name="Oobe_MonetSource" xml:space="preserve"><value>莫奈取色来源</value></data>
|
||||
<data name="Oobe_MonetFromWallpaper" xml:space="preserve"><value>从桌面壁纸取色</value></data>
|
||||
<data name="Oobe_MonetFromCustomImage" xml:space="preserve"><value>自定义图片取色</value></data>
|
||||
<data name="Oobe_MonetDisabled" xml:space="preserve"><value>不使用莫奈取色</value></data>
|
||||
<data name="Oobe_DataLocationTitle" xml:space="preserve"><value>选择数据保存位置</value></data>
|
||||
<data name="Oobe_SystemProfile" xml:space="preserve"><value>保存在系统用户目录(推荐)</value></data>
|
||||
<data name="Oobe_SystemProfileDesc" xml:space="preserve"><value>数据与当前 Windows 用户绑定,在应用重新安装和更新后保持完整。</value></data>
|
||||
<data name="Oobe_Portable" xml:space="preserve"><value>保存在应用安装目录(便携模式)</value></data>
|
||||
<data name="Oobe_PortableDesc" xml:space="preserve"><value>适用于便携安装。整个应用文件夹可以连同数据一起移动到另一台机器。</value></data>
|
||||
<data name="Oobe_NotWritable" xml:space="preserve"><value>无法保存到应用目录</value></data>
|
||||
<data name="Oobe_NotWritableDesc" xml:space="preserve"><value>当前安装目录需要管理员权限才能写入。数据将存储在系统用户目录中。</value></data>
|
||||
<data name="Oobe_StartupTitle" xml:space="preserve"><value>启动与展示</value></data>
|
||||
<data name="Oobe_ShowInTaskbar" xml:space="preserve"><value>在任务栏显示主桌面窗口</value></data>
|
||||
<data name="Oobe_SlideTransition" xml:space="preserve"><value>以滑动方式显示主窗口</value></data>
|
||||
<data name="Oobe_FadeTransition" xml:space="preserve"><value>启动时使用淡入过渡</value></data>
|
||||
<data name="Oobe_FusedDesktop" xml:space="preserve"><value>融合桌面与弹入手势</value></data>
|
||||
<data name="Oobe_AutoStart" xml:space="preserve"><value>登录 Windows 时自动启动阑山桌面</value></data>
|
||||
<data name="Oobe_PrivacyTitle" xml:space="preserve"><value>信息与隐私</value></data>
|
||||
<data name="Oobe_CrashReports" xml:space="preserve"><value>发送匿名崩溃报告</value></data>
|
||||
<data name="Oobe_UsageStats" xml:space="preserve"><value>发送匿名使用统计</value></data>
|
||||
<data name="Oobe_PrivacyTrackingId" xml:space="preserve"><value>隐私追踪 ID</value></data>
|
||||
<data name="Oobe_Agree" xml:space="preserve"><value>同意</value></data>
|
||||
<data name="Oobe_PrivacyPolicyLink" xml:space="preserve"><value>《阑山桌面遥测隐私数据收集协议》</value></data>
|
||||
<data name="Oobe_ButtonBack" xml:space="preserve"><value>返回</value></data>
|
||||
<data name="Oobe_ButtonNext" xml:space="preserve"><value>下一步</value></data>
|
||||
<data name="Oobe_CompleteTitle" xml:space="preserve"><value>欢迎使用阑山桌面</value></data>
|
||||
<data name="Oobe_CompleteSubtitle" xml:space="preserve"><value>你的桌面,不止一面</value></data>
|
||||
|
||||
<!-- MigrationPromptWindow -->
|
||||
<data name="Migration_Title" xml:space="preserve"><value>阑山桌面 - 版本迁移</value></data>
|
||||
<data name="Migration_DetectedOldVersion" xml:space="preserve"><value>检测到旧版本</value></data>
|
||||
<data name="Migration_DetectedDesc" xml:space="preserve"><value>检测到您的系统中安装了旧版本的阑山桌面(0.8.4)...</value></data>
|
||||
<data name="Migration_Version" xml:space="preserve"><value>版本:</value></data>
|
||||
<data name="Migration_Location" xml:space="preserve"><value>位置:</value></data>
|
||||
<data name="Migration_Type" xml:space="preserve"><value>类型:</value></data>
|
||||
<data name="Migration_Installed" xml:space="preserve"><value>安装版</value></data>
|
||||
<data name="Migration_UninstallNote" xml:space="preserve"><value>卸载旧版本不会影响新版本的使用,您的个人数据将保留。</value></data>
|
||||
<data name="Migration_ButtonViewLocation" xml:space="preserve"><value>查看位置</value></data>
|
||||
<data name="Migration_ButtonSkip" xml:space="preserve"><value>暂不处理</value></data>
|
||||
<data name="Migration_ButtonUninstall" xml:space="preserve"><value>卸载旧版本</value></data>
|
||||
|
||||
<!-- PrivacyPolicyWindow -->
|
||||
<data name="Privacy_Title" xml:space="preserve"><value>阑山桌面遥测隐私数据收集协议</value></data>
|
||||
<data name="Privacy_Header" xml:space="preserve"><value>阑山桌面遥测隐私数据收集协议</value></data>
|
||||
<data name="Privacy_Description" xml:space="preserve"><value>请仔细阅读以下协议内容,了解我们如何收集、使用和保护您的数据</value></data>
|
||||
<data name="Privacy_ButtonClose" xml:space="preserve"><value>关闭</value></data>
|
||||
|
||||
<!-- DevDebugWindow -->
|
||||
<data name="DevDebug_Title" xml:space="preserve"><value>开发调试窗口</value></data>
|
||||
<data name="DevDebug_Splash" xml:space="preserve"><value>启动画面</value></data>
|
||||
<data name="DevDebug_Error" xml:space="preserve"><value>错误页面</value></data>
|
||||
<data name="DevDebug_Update" xml:space="preserve"><value>更新页面</value></data>
|
||||
<data name="DevDebug_Oobe" xml:space="preserve"><value>OOBE页面</value></data>
|
||||
<data name="DevDebug_DataLocation" xml:space="preserve"><value>数据位置选择</value></data>
|
||||
<data name="DevDebug_EnableFeature" xml:space="preserve"><value>启用功能</value></data>
|
||||
<data name="DevDebug_Open" xml:space="preserve"><value>打开</value></data>
|
||||
<data name="DevDebug_SetAllViewMode" xml:space="preserve"><value>全部设为查看模式</value></data>
|
||||
<data name="DevDebug_SetAllFunctionMode" xml:space="preserve"><value>全部设为功能模式</value></data>
|
||||
<data name="DevDebug_Close" xml:space="preserve"><value>关闭</value></data>
|
||||
|
||||
<!-- LauncherFlowCoordinator -->
|
||||
<data name="Coordinator_SlowDeviceMessage" xml:space="preserve"><value>设备较慢,仍在启动,请稍候。</value></data>
|
||||
<data name="Coordinator_RunningHostMessage" xml:space="preserve"><value>桌面主进程仍在运行,Launcher 会继续等待,不会重复启动。</value></data>
|
||||
|
||||
<!-- App.axaml.cs preview strings -->
|
||||
<data name="Preview_SplashInitializing" xml:space="preserve"><value>正在初始化...</value></data>
|
||||
<data name="Preview_SplashCheckingUpdates" xml:space="preserve"><value>正在检查更新...</value></data>
|
||||
<data name="Preview_SplashCheckingPlugins" xml:space="preserve"><value>正在检查插件...</value></data>
|
||||
<data name="Preview_SplashLaunchingHost" xml:space="preserve"><value>正在启动主程序...</value></data>
|
||||
<data name="Preview_SplashReady" xml:space="preserve"><value>准备就绪</value></data>
|
||||
<data name="Preview_ErrorMessage" xml:space="preserve"><value>[预览] 这是启动器错误窗口预览。</value></data>
|
||||
<data name="Preview_UpdateProcessing" xml:space="preserve"><value>正在处理 {0}...</value></data>
|
||||
<data name="Preview_ActivationConnecting" xml:space="preserve"><value>正在连接到活跃的启动器...</value></data>
|
||||
</root>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建 en-US RESX 文件**
|
||||
|
||||
创建 `Resources/Strings.en-US.resx`,包含所有字符串的英文翻译。结构与默认文件相同,仅 `<value>` 内容为英文。
|
||||
|
||||
```xml
|
||||
<!-- 示例条目 -->
|
||||
<data name="Splash_Title" xml:space="preserve"><value>LanMountain Desktop</value></data>
|
||||
<data name="Splash_AppName" xml:space="preserve"><value>LanMountain Desktop</value></data>
|
||||
<data name="Splash_StatusInitializing" xml:space="preserve"><value>Initializing...</value></data>
|
||||
<data name="Error_TitleCannotConfirm" xml:space="preserve"><value>Launcher could not confirm startup</value></data>
|
||||
<data name="Error_MessageNotReached" xml:space="preserve"><value>LanMountain Desktop did not reach the expected startup state.</value></data>
|
||||
<!-- ... 所有键的英文翻译 ... -->
|
||||
```
|
||||
|
||||
- [ ] **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` 的 `<ItemGroup>` 中添加:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Resources\Strings.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
注意:使用 `PublicResXFileCodeGenerator` 而非 `ResXFileCodeGenerator`,生成 `public` 类以便 AXAML 的 `x:Static` 可以访问。
|
||||
|
||||
- [ ] **Step 6: 修改 AOT props 添加资源程序集保留**
|
||||
|
||||
在 `LanMountainDesktop.Launcher.AOT.props` 的 AOT 修剪配置 `<ItemGroup>` 中添加:
|
||||
|
||||
```xml
|
||||
<TrimmerRootAssembly Include="LanMountainDesktop.Launcher" />
|
||||
```
|
||||
|
||||
- [ ] **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<string>(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 命名空间并替换字符串**
|
||||
|
||||
在 `<Window>` 标签添加命名空间:
|
||||
```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# 中或使用独立资源文件。
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
13
.trae/specs/clock-air-app-mvp/checklist.md
Normal file
13
.trae/specs/clock-air-app-mvp/checklist.md
Normal file
@@ -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.
|
||||
42
.trae/specs/clock-air-app-mvp/spec.md
Normal file
42
.trae/specs/clock-air-app-mvp/spec.md
Normal file
@@ -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.
|
||||
15
.trae/specs/clock-air-app-mvp/tasks.md
Normal file
15
.trae/specs/clock-air-app-mvp/tasks.md
Normal file
@@ -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.
|
||||
@@ -1,64 +1,16 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
<faWindowing:FAAppWindow xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:faWindowing="using:FluentAvalonia.UI.Windowing"
|
||||
x:Class="LanMountainDesktop.AirAppHost.AirAppWindow"
|
||||
Width="520"
|
||||
Height="360"
|
||||
MinWidth="360"
|
||||
MinHeight="260"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
WindowDecorations="None"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaTitleBarHeightHint="-1"
|
||||
TransparencyLevelHint="Transparent"
|
||||
Background="Transparent"
|
||||
FontFamily="{DynamicResource AppFontFamily}"
|
||||
Title="Air APP">
|
||||
<Border x:Name="WindowShell"
|
||||
Background="{DynamicResource AirAppWindowBackgroundBrush}"
|
||||
BorderBrush="{DynamicResource AirAppWindowBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="18"
|
||||
ClipToBounds="True"
|
||||
BoxShadow="0 18 44 #22000000">
|
||||
<Grid RowDefinitions="52,*">
|
||||
<Grid x:Name="TitleBar"
|
||||
ColumnDefinitions="*,Auto"
|
||||
Background="Transparent"
|
||||
PointerPressed="OnTitleBarPointerPressed">
|
||||
<StackPanel Margin="18,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="TitleTextBlock"
|
||||
Text="Air APP"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AirAppTitleTextBrush}" />
|
||||
<TextBlock x:Name="SubtitleTextBlock"
|
||||
Text="LanMountainDesktop"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource AirAppSecondaryTextBrush}" />
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="1"
|
||||
Width="36"
|
||||
Height="36"
|
||||
Margin="0,8,10,8"
|
||||
Padding="0"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0"
|
||||
Click="OnCloseClick">
|
||||
<TextBlock Text="X"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource AirAppTitleTextBrush}" />
|
||||
</Button>
|
||||
<Grid x:Name="WindowRoot"
|
||||
Background="{DynamicResource AirAppWindowBackgroundBrush}">
|
||||
<ContentControl x:Name="ContentHost" />
|
||||
</Grid>
|
||||
|
||||
<ContentControl x:Name="ContentHost"
|
||||
Grid.Row="1" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
</faWindowing:FAAppWindow>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
310
LanMountainDesktop.AirAppHost/ClockAirAppView.axaml
Normal file
310
LanMountainDesktop.AirAppHost/ClockAirAppView.axaml
Normal file
@@ -0,0 +1,310 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LanMountainDesktop.AirAppHost.ClockAirAppView">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Border.clock-card">
|
||||
<Setter Property="Background" Value="#F7FFFFFF" />
|
||||
<Setter Property="BorderBrush" Value="#16000000" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="18" />
|
||||
<Setter Property="Padding" Value="18" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ToggleButton.clock-tab">
|
||||
<Setter Property="MinWidth" Value="84" />
|
||||
<Setter Property="Height" Value="34" />
|
||||
<Setter Property="Padding" Value="14,0" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AirAppSecondaryTextBrush}" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ToggleButton.clock-tab:checked">
|
||||
<Setter Property="Background" Value="{DynamicResource AirAppAccentBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.clock-command">
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
<Setter Property="Padding" Value="14,6" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.clock-icon-command">
|
||||
<Setter Property="Width" Value="34" />
|
||||
<Setter Property="Height" Value="30" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.clock-muted">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AirAppSecondaryTextBrush}" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
RowSpacing="16"
|
||||
Margin="22,14,22,20">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Spacing="3"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock x:Name="HeaderTitleTextBlock"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AirAppTitleTextBrush}" />
|
||||
<TextBlock x:Name="HeaderSubtitleTextBlock"
|
||||
Classes="clock-muted"
|
||||
FontSize="12" />
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Column="1"
|
||||
Background="#0A000000"
|
||||
CornerRadius="14"
|
||||
Padding="4"
|
||||
VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="4">
|
||||
<ToggleButton x:Name="WorldTabButton"
|
||||
Classes="clock-tab"
|
||||
Tag="world"
|
||||
Click="OnTabButtonClick" />
|
||||
<ToggleButton x:Name="StopwatchTabButton"
|
||||
Classes="clock-tab"
|
||||
Tag="stopwatch"
|
||||
Click="OnTabButtonClick" />
|
||||
<ToggleButton x:Name="TimerTabButton"
|
||||
Classes="clock-tab"
|
||||
Tag="timer"
|
||||
Click="OnTabButtonClick" />
|
||||
<ToggleButton x:Name="SettingsTabButton"
|
||||
Classes="clock-tab"
|
||||
Tag="settings"
|
||||
Click="OnTabButtonClick" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid x:Name="WorldPage"
|
||||
ColumnDefinitions="1.05*,1.1*"
|
||||
ColumnSpacing="16">
|
||||
<Border Classes="clock-card">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<TextBlock x:Name="LocalLabelTextBlock"
|
||||
Classes="clock-muted"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold" />
|
||||
<StackPanel Grid.Row="1"
|
||||
Spacing="12"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock x:Name="LocalTimeTextBlock"
|
||||
FontSize="54"
|
||||
FontWeight="SemiBold"
|
||||
LetterSpacing="0"
|
||||
Foreground="{DynamicResource AirAppTitleTextBrush}" />
|
||||
<TextBlock x:Name="LocalDateTextBlock"
|
||||
Classes="clock-muted"
|
||||
FontSize="15" />
|
||||
<TextBlock x:Name="LocalTimeZoneTextBlock"
|
||||
Classes="clock-muted"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="WorldSummaryTextBlock"
|
||||
Grid.Row="2"
|
||||
Classes="clock-muted"
|
||||
FontSize="12" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="1"
|
||||
Classes="clock-card">
|
||||
<Grid RowDefinitions="Auto,Auto,*"
|
||||
RowSpacing="12">
|
||||
<Grid ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="8">
|
||||
<TextBox x:Name="TimeZoneSearchTextBox"
|
||||
PlaceholderText="Search"
|
||||
TextChanged="OnTimeZoneSearchChanged" />
|
||||
<Button x:Name="AddCityButton"
|
||||
Grid.Column="1"
|
||||
Classes="clock-command"
|
||||
Click="OnAddCityClick" />
|
||||
</Grid>
|
||||
|
||||
<ComboBox x:Name="TimeZoneComboBox"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
MaxDropDownHeight="280" />
|
||||
|
||||
<ScrollViewer Grid.Row="2"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="WorldClockRowsPanel"
|
||||
Spacing="8" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Border x:Name="StopwatchPage"
|
||||
Classes="clock-card">
|
||||
<Grid RowDefinitions="Auto,Auto,*"
|
||||
RowSpacing="18">
|
||||
<TextBlock x:Name="StopwatchHintTextBlock"
|
||||
Classes="clock-muted"
|
||||
FontSize="13" />
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Spacing="18"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBlock x:Name="StopwatchElapsedTextBlock"
|
||||
Text="00:00:00.00"
|
||||
FontSize="58"
|
||||
FontWeight="SemiBold"
|
||||
LetterSpacing="0"
|
||||
Foreground="{DynamicResource AirAppTitleTextBrush}" />
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="10">
|
||||
<Button x:Name="StopwatchStartPauseButton"
|
||||
Classes="clock-command"
|
||||
Click="OnStopwatchStartPauseClick" />
|
||||
<Button x:Name="StopwatchLapButton"
|
||||
Classes="clock-command"
|
||||
Click="OnStopwatchLapClick" />
|
||||
<Button x:Name="StopwatchResetButton"
|
||||
Classes="clock-command"
|
||||
Click="OnStopwatchResetClick" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="2"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="StopwatchLapsPanel"
|
||||
Spacing="6" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="TimerPage"
|
||||
Classes="clock-card">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*"
|
||||
RowSpacing="18">
|
||||
<TextBlock x:Name="TimerHintTextBlock"
|
||||
Classes="clock-muted"
|
||||
FontSize="13" />
|
||||
|
||||
<TextBlock x:Name="TimerRemainingTextBlock"
|
||||
Grid.Row="1"
|
||||
Text="00:05:00"
|
||||
FontSize="58"
|
||||
FontWeight="SemiBold"
|
||||
LetterSpacing="0"
|
||||
Foreground="{DynamicResource AirAppTitleTextBrush}"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="8">
|
||||
<Button Classes="clock-command"
|
||||
Tag="1"
|
||||
Click="OnTimerPresetClick">1</Button>
|
||||
<Button Classes="clock-command"
|
||||
Tag="5"
|
||||
Click="OnTimerPresetClick">5</Button>
|
||||
<Button Classes="clock-command"
|
||||
Tag="10"
|
||||
Click="OnTimerPresetClick">10</Button>
|
||||
<Button Classes="clock-command"
|
||||
Tag="15"
|
||||
Click="OnTimerPresetClick">15</Button>
|
||||
<Button Classes="clock-command"
|
||||
Tag="30"
|
||||
Click="OnTimerPresetClick">30</Button>
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="3"
|
||||
RowDefinitions="Auto,Auto,Auto"
|
||||
RowSpacing="14"
|
||||
HorizontalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBox x:Name="TimerMinutesTextBox"
|
||||
Width="120"
|
||||
PlaceholderText="Minutes"
|
||||
Text="5" />
|
||||
<Button x:Name="TimerApplyButton"
|
||||
Classes="clock-command"
|
||||
Click="OnTimerApplyClick" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="10">
|
||||
<Button x:Name="TimerStartPauseButton"
|
||||
Classes="clock-command"
|
||||
Click="OnTimerStartPauseClick" />
|
||||
<Button x:Name="TimerResetButton"
|
||||
Classes="clock-command"
|
||||
Click="OnTimerResetClick" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock x:Name="TimerStatusTextBlock"
|
||||
Grid.Row="2"
|
||||
Classes="clock-muted"
|
||||
FontSize="13"
|
||||
HorizontalAlignment="Center" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="SettingsPage"
|
||||
Classes="clock-card">
|
||||
<StackPanel Spacing="18"
|
||||
MaxWidth="560"
|
||||
HorizontalAlignment="Left">
|
||||
<TextBlock x:Name="SettingsHeaderTextBlock"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AirAppTitleTextBrush}" />
|
||||
|
||||
<Grid ColumnDefinitions="220,*"
|
||||
RowDefinitions="Auto,Auto,Auto,Auto"
|
||||
RowSpacing="14"
|
||||
ColumnSpacing="18">
|
||||
<TextBlock x:Name="TimeFormatLabelTextBlock"
|
||||
Classes="clock-muted"
|
||||
VerticalAlignment="Center" />
|
||||
<ComboBox x:Name="TimeFormatComboBox"
|
||||
Grid.Column="1"
|
||||
SelectionChanged="OnSettingsChanged" />
|
||||
|
||||
<TextBlock x:Name="StartupTabLabelTextBlock"
|
||||
Grid.Row="1"
|
||||
Classes="clock-muted"
|
||||
VerticalAlignment="Center" />
|
||||
<ComboBox x:Name="StartupTabComboBox"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
SelectionChanged="OnSettingsChanged" />
|
||||
|
||||
<CheckBox x:Name="ShowSecondsCheckBox"
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
IsCheckedChanged="OnSettingsChanged" />
|
||||
<CheckBox x:Name="ActivateOnTimerFinishedCheckBox"
|
||||
Grid.Row="3"
|
||||
Grid.ColumnSpan="2"
|
||||
IsCheckedChanged="OnSettingsChanged" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
665
LanMountainDesktop.AirAppHost/ClockAirAppView.axaml.cs
Normal file
665
LanMountainDesktop.AirAppHost/ClockAirAppView.axaml.cs
Normal file
@@ -0,0 +1,665 @@
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.ClockAirApp;
|
||||
|
||||
namespace LanMountainDesktop.AirAppHost;
|
||||
|
||||
public sealed partial class ClockAirAppView : UserControl
|
||||
{
|
||||
private sealed class WorldClockRowVisual
|
||||
{
|
||||
public required TimeZoneInfo TimeZone { get; init; }
|
||||
|
||||
public required TextBlock TimeTextBlock { get; init; }
|
||||
|
||||
public required TextBlock DateTextBlock { get; init; }
|
||||
|
||||
public required TextBlock OffsetTextBlock { get; init; }
|
||||
}
|
||||
|
||||
private readonly DispatcherTimer _clockTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(250)
|
||||
};
|
||||
|
||||
private readonly AirAppLaunchOptions _options;
|
||||
private readonly ClockAirAppSettingsStore _settingsStore = new();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly ClockAirAppStopwatchState _stopwatchState = new();
|
||||
private readonly ClockAirAppTimerState _timerState = new();
|
||||
private readonly List<TimeZoneInfo> _allTimeZones;
|
||||
private readonly List<WorldClockRowVisual> _worldClockRows = [];
|
||||
|
||||
private ClockAirAppSettingsSnapshot _settings = ClockAirAppSettingsSnapshot.Normalize(null);
|
||||
private CultureInfo _culture = CultureInfo.CurrentCulture;
|
||||
private string _languageCode = "zh-CN";
|
||||
private string _selectedTab = ClockAirAppTabIds.WorldClock;
|
||||
private bool _suppressSettingsEvents;
|
||||
|
||||
public ClockAirAppView()
|
||||
: this(AirAppLaunchOptions.Parse([]))
|
||||
{
|
||||
}
|
||||
|
||||
public ClockAirAppView(AirAppLaunchOptions options)
|
||||
{
|
||||
_options = options;
|
||||
_allTimeZones = TimeZoneInfo.GetSystemTimeZones()
|
||||
.OrderBy(static zone => zone.GetUtcOffset(DateTime.UtcNow))
|
||||
.ThenBy(static zone => zone.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
InitializeComponent();
|
||||
LoadLanguage();
|
||||
LoadSettings();
|
||||
ApplyLocalizedText();
|
||||
PopulateSettingsControls();
|
||||
PopulateTimeZoneCombo();
|
||||
RebuildWorldClockRows();
|
||||
SelectStartupTab();
|
||||
UpdateAll();
|
||||
|
||||
_clockTimer.Tick += OnClockTimerTick;
|
||||
AttachedToVisualTree += (_, _) =>
|
||||
{
|
||||
UpdateAll();
|
||||
_clockTimer.Start();
|
||||
};
|
||||
DetachedFromVisualTree += (_, _) => _clockTimer.Stop();
|
||||
}
|
||||
|
||||
private void LoadLanguage()
|
||||
{
|
||||
try
|
||||
{
|
||||
var appSettings = new AppSettingsService().Load();
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(appSettings.LanguageCode);
|
||||
_culture = CultureInfo.GetCultureInfo(_languageCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_languageCode = "zh-CN";
|
||||
_culture = CultureInfo.GetCultureInfo("zh-CN");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
_settings = _settingsStore.Load();
|
||||
_timerState.SetDuration(TimeSpan.FromMinutes(5));
|
||||
}
|
||||
|
||||
private void ApplyLocalizedText()
|
||||
{
|
||||
HeaderTitleTextBlock.Text = L("clockairapp.title", "Clock");
|
||||
HeaderSubtitleTextBlock.Text = L("clockairapp.subtitle", "World clock, stopwatch and timer");
|
||||
|
||||
WorldTabButton.Content = L("clockairapp.tab.world", "World");
|
||||
StopwatchTabButton.Content = L("clockairapp.tab.stopwatch", "Stopwatch");
|
||||
TimerTabButton.Content = L("clockairapp.tab.timer", "Timer");
|
||||
SettingsTabButton.Content = L("clockairapp.tab.settings", "Settings");
|
||||
|
||||
LocalLabelTextBlock.Text = L("clockairapp.world.local", "Local time");
|
||||
AddCityButton.Content = L("clockairapp.world.add", "Add");
|
||||
TimeZoneSearchTextBox.PlaceholderText = L("clockairapp.world.search", "Search city or time zone");
|
||||
|
||||
StopwatchHintTextBlock.Text = L("clockairapp.stopwatch.hint", "Lap timing stays in this window session.");
|
||||
StopwatchStartPauseButton.Content = L("clockairapp.action.start", "Start");
|
||||
StopwatchLapButton.Content = L("clockairapp.stopwatch.lap", "Lap");
|
||||
StopwatchResetButton.Content = L("clockairapp.action.reset", "Reset");
|
||||
|
||||
TimerHintTextBlock.Text = L("clockairapp.timer.hint", "Choose a preset or enter custom minutes.");
|
||||
TimerApplyButton.Content = L("clockairapp.timer.apply", "Apply");
|
||||
TimerStartPauseButton.Content = L("clockairapp.action.start", "Start");
|
||||
TimerResetButton.Content = L("clockairapp.action.reset", "Reset");
|
||||
TimerMinutesTextBox.PlaceholderText = L("clockairapp.timer.minutes", "Minutes");
|
||||
|
||||
SettingsHeaderTextBlock.Text = L("clockairapp.settings.title", "Clock settings");
|
||||
TimeFormatLabelTextBlock.Text = L("clockairapp.settings.time_format", "Time format");
|
||||
StartupTabLabelTextBlock.Text = L("clockairapp.settings.startup_tab", "Startup page");
|
||||
ShowSecondsCheckBox.Content = L("clockairapp.settings.show_seconds", "Show seconds");
|
||||
ActivateOnTimerFinishedCheckBox.Content = L("clockairapp.settings.activate_timer", "Activate window when timer finishes");
|
||||
}
|
||||
|
||||
private void PopulateSettingsControls()
|
||||
{
|
||||
_suppressSettingsEvents = true;
|
||||
try
|
||||
{
|
||||
SetComboItems(
|
||||
TimeFormatComboBox,
|
||||
[
|
||||
(ClockAirAppTimeFormatMode.System, L("clockairapp.settings.time_format.system", "Follow system")),
|
||||
(ClockAirAppTimeFormatMode.TwentyFourHour, L("clockairapp.settings.time_format.24h", "24-hour")),
|
||||
(ClockAirAppTimeFormatMode.TwelveHour, L("clockairapp.settings.time_format.12h", "12-hour"))
|
||||
],
|
||||
_settings.TimeFormatMode);
|
||||
SetComboItems(
|
||||
StartupTabComboBox,
|
||||
[
|
||||
(ClockAirAppTabIds.Last, L("clockairapp.settings.startup.last", "Last used")),
|
||||
(ClockAirAppTabIds.WorldClock, L("clockairapp.tab.world", "World")),
|
||||
(ClockAirAppTabIds.Stopwatch, L("clockairapp.tab.stopwatch", "Stopwatch")),
|
||||
(ClockAirAppTabIds.Timer, L("clockairapp.tab.timer", "Timer"))
|
||||
],
|
||||
_settings.StartupTab);
|
||||
ShowSecondsCheckBox.IsChecked = _settings.ShowSeconds;
|
||||
ActivateOnTimerFinishedCheckBox.IsChecked = _settings.ActivateOnTimerFinished;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressSettingsEvents = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetComboItems(ComboBox comboBox, IEnumerable<(string Id, string Text)> items, string selectedId)
|
||||
{
|
||||
comboBox.Items.Clear();
|
||||
foreach (var item in items)
|
||||
{
|
||||
comboBox.Items.Add(new ComboBoxItem
|
||||
{
|
||||
Tag = item.Id,
|
||||
Content = item.Text
|
||||
});
|
||||
}
|
||||
|
||||
comboBox.SelectedItem = comboBox.Items
|
||||
.OfType<ComboBoxItem>()
|
||||
.FirstOrDefault(item => string.Equals(item.Tag as string, selectedId, StringComparison.OrdinalIgnoreCase))
|
||||
?? comboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
|
||||
}
|
||||
|
||||
private void SelectStartupTab()
|
||||
{
|
||||
var startupTab = ClockAirAppTabIds.Normalize(_settings.StartupTab, ClockAirAppTabIds.Last);
|
||||
var tab = string.Equals(startupTab, ClockAirAppTabIds.Last, StringComparison.OrdinalIgnoreCase)
|
||||
? ClockAirAppTabIds.Normalize(_settings.LastSelectedTab)
|
||||
: ClockAirAppTabIds.Normalize(startupTab);
|
||||
SelectTab(tab, save: false);
|
||||
}
|
||||
|
||||
private void OnClockTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
UpdateAll();
|
||||
}
|
||||
|
||||
private void UpdateAll()
|
||||
{
|
||||
var now = DateTimeOffset.Now;
|
||||
UpdateWorldClock(now);
|
||||
UpdateStopwatch(now);
|
||||
UpdateTimer(now);
|
||||
}
|
||||
|
||||
private void UpdateWorldClock(DateTimeOffset now)
|
||||
{
|
||||
var localNow = now.LocalDateTime;
|
||||
LocalTimeTextBlock.Text = ClockAirAppTimeFormatter.FormatTime(localNow, _settings, _culture);
|
||||
LocalDateTextBlock.Text = localNow.ToString("yyyy-MM-dd dddd", _culture);
|
||||
LocalTimeZoneTextBlock.Text = TimeZoneInfo.Local.DisplayName;
|
||||
WorldSummaryTextBlock.Text = Lf("clockairapp.world.count", "{0} cities", _settings.WorldClockTimeZoneIds.Count);
|
||||
|
||||
var utcNow = now.UtcDateTime;
|
||||
foreach (var row in _worldClockRows)
|
||||
{
|
||||
var zonedTime = TimeZoneInfo.ConvertTimeFromUtc(utcNow, row.TimeZone);
|
||||
row.TimeTextBlock.Text = ClockAirAppTimeFormatter.FormatTime(zonedTime, _settings, _culture);
|
||||
row.DateTextBlock.Text = $"{ResolveRelativeDayLabel((zonedTime.Date - localNow.Date).Days)} - {zonedTime.ToString("yyyy-MM-dd", _culture)}";
|
||||
row.OffsetTextBlock.Text = ClockAirAppTimeFormatter.FormatUtcOffset(row.TimeZone.GetUtcOffset(utcNow));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStopwatch(DateTimeOffset now)
|
||||
{
|
||||
StopwatchElapsedTextBlock.Text = ClockAirAppTimeFormatter.FormatDuration(_stopwatchState.GetElapsed(now), includeMilliseconds: true);
|
||||
StopwatchStartPauseButton.Content = _stopwatchState.IsRunning
|
||||
? L("clockairapp.action.pause", "Pause")
|
||||
: L("clockairapp.action.start", "Start");
|
||||
StopwatchLapButton.IsEnabled = _stopwatchState.GetElapsed(now) > TimeSpan.Zero;
|
||||
StopwatchResetButton.IsEnabled = _stopwatchState.GetElapsed(now) > TimeSpan.Zero || _stopwatchState.Laps.Count > 0;
|
||||
}
|
||||
|
||||
private void UpdateTimer(DateTimeOffset now)
|
||||
{
|
||||
if (_timerState.Update(now))
|
||||
{
|
||||
TimerStatusTextBlock.Text = L("clockairapp.timer.finished", "Timer finished");
|
||||
if (_settings.ActivateOnTimerFinished && VisualRoot is Window window)
|
||||
{
|
||||
window.Activate();
|
||||
}
|
||||
}
|
||||
|
||||
TimerRemainingTextBlock.Text = ClockAirAppTimeFormatter.FormatDuration(_timerState.GetRemaining(now));
|
||||
TimerStartPauseButton.Content = _timerState.IsRunning
|
||||
? L("clockairapp.action.pause", "Pause")
|
||||
: L("clockairapp.action.start", "Start");
|
||||
TimerResetButton.IsEnabled = _timerState.GetRemaining(now) < _timerState.Duration || _timerState.IsCompleted;
|
||||
if (!_timerState.IsCompleted && string.IsNullOrWhiteSpace(TimerStatusTextBlock.Text))
|
||||
{
|
||||
TimerStatusTextBlock.Text = Lf("clockairapp.timer.duration_status", "Duration {0}", ClockAirAppTimeFormatter.FormatDuration(_timerState.Duration));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTabButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is ToggleButton button && button.Tag is string tab)
|
||||
{
|
||||
SelectTab(tab, save: true);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectTab(string tab, bool save)
|
||||
{
|
||||
_selectedTab = ClockAirAppTabIds.Normalize(tab);
|
||||
WorldPage.IsVisible = string.Equals(_selectedTab, ClockAirAppTabIds.WorldClock, StringComparison.OrdinalIgnoreCase);
|
||||
StopwatchPage.IsVisible = string.Equals(_selectedTab, ClockAirAppTabIds.Stopwatch, StringComparison.OrdinalIgnoreCase);
|
||||
TimerPage.IsVisible = string.Equals(_selectedTab, ClockAirAppTabIds.Timer, StringComparison.OrdinalIgnoreCase);
|
||||
SettingsPage.IsVisible = string.Equals(_selectedTab, ClockAirAppTabIds.Settings, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
WorldTabButton.IsChecked = WorldPage.IsVisible;
|
||||
StopwatchTabButton.IsChecked = StopwatchPage.IsVisible;
|
||||
TimerTabButton.IsChecked = TimerPage.IsVisible;
|
||||
SettingsTabButton.IsChecked = SettingsPage.IsVisible;
|
||||
|
||||
if (save)
|
||||
{
|
||||
_settings.LastSelectedTab = _selectedTab;
|
||||
_settingsStore.Save(_settings);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTimeZoneSearchChanged(object? sender, TextChangedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
PopulateTimeZoneCombo();
|
||||
}
|
||||
|
||||
private void PopulateTimeZoneCombo()
|
||||
{
|
||||
var query = TimeZoneSearchTextBox.Text?.Trim() ?? string.Empty;
|
||||
var zones = _allTimeZones
|
||||
.Where(zone => MatchesTimeZoneQuery(zone, query))
|
||||
.Take(80)
|
||||
.ToList();
|
||||
|
||||
TimeZoneComboBox.Items.Clear();
|
||||
foreach (var zone in zones)
|
||||
{
|
||||
TimeZoneComboBox.Items.Add(new ComboBoxItem
|
||||
{
|
||||
Tag = zone.Id,
|
||||
Content = FormatTimeZoneOption(zone)
|
||||
});
|
||||
}
|
||||
|
||||
TimeZoneComboBox.SelectedItem = TimeZoneComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
|
||||
}
|
||||
|
||||
private bool MatchesTimeZoneQuery(TimeZoneInfo zone, string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var cityName = ClockAirAppTimeFormatter.ResolveCityName(zone, _languageCode);
|
||||
return zone.Id.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
zone.DisplayName.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
zone.StandardName.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
cityName.Contains(query, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private string FormatTimeZoneOption(TimeZoneInfo zone)
|
||||
{
|
||||
return $"{ClockAirAppTimeFormatter.FormatUtcOffset(zone.GetUtcOffset(DateTime.UtcNow))} | {ClockAirAppTimeFormatter.ResolveCityName(zone, _languageCode)} | {zone.StandardName}";
|
||||
}
|
||||
|
||||
private void OnAddCityClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (TimeZoneComboBox.SelectedItem is not ComboBoxItem item || item.Tag is not string zoneId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_settings.WorldClockTimeZoneIds.Any(existing => string.Equals(existing, zoneId, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settings.WorldClockTimeZoneIds.Add(zoneId);
|
||||
SaveWorldClockSettings();
|
||||
}
|
||||
|
||||
private void RebuildWorldClockRows()
|
||||
{
|
||||
_worldClockRows.Clear();
|
||||
WorldClockRowsPanel.Children.Clear();
|
||||
for (var index = 0; index < _settings.WorldClockTimeZoneIds.Count; index++)
|
||||
{
|
||||
var timeZone = WorldClockTimeZoneCatalog.ResolveTimeZoneOrLocal(_settings.WorldClockTimeZoneIds[index]);
|
||||
AddWorldClockRow(timeZone, index);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddWorldClockRow(TimeZoneInfo timeZone, int index)
|
||||
{
|
||||
var cityText = new TextBlock
|
||||
{
|
||||
Text = ClockAirAppTimeFormatter.ResolveCityName(timeZone, _languageCode),
|
||||
FontSize = 15,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = TryGetBrush("AirAppTitleTextBrush", "#FF171A20")
|
||||
};
|
||||
var timeText = new TextBlock
|
||||
{
|
||||
FontSize = 24,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
LetterSpacing = 0,
|
||||
Foreground = TryGetBrush("AirAppTitleTextBrush", "#FF171A20"),
|
||||
HorizontalAlignment = HorizontalAlignment.Right
|
||||
};
|
||||
var dateText = new TextBlock
|
||||
{
|
||||
FontSize = 12,
|
||||
Foreground = TryGetBrush("AirAppSecondaryTextBrush", "#FF657080")
|
||||
};
|
||||
var offsetText = new TextBlock
|
||||
{
|
||||
FontSize = 12,
|
||||
Foreground = TryGetBrush("AirAppSecondaryTextBrush", "#FF657080"),
|
||||
HorizontalAlignment = HorizontalAlignment.Right
|
||||
};
|
||||
|
||||
var upButton = CreateIconButton("↑", L("clockairapp.action.move_up", "Move up"));
|
||||
upButton.IsEnabled = index > 0;
|
||||
upButton.Click += (_, _) => MoveWorldClock(index, -1);
|
||||
|
||||
var downButton = CreateIconButton("↓", L("clockairapp.action.move_down", "Move down"));
|
||||
downButton.IsEnabled = index < _settings.WorldClockTimeZoneIds.Count - 1;
|
||||
downButton.Click += (_, _) => MoveWorldClock(index, 1);
|
||||
|
||||
var removeButton = CreateIconButton("×", L("clockairapp.action.remove", "Remove"));
|
||||
removeButton.IsEnabled = _settings.WorldClockTimeZoneIds.Count > 1;
|
||||
removeButton.Click += (_, _) => RemoveWorldClock(index);
|
||||
|
||||
var row = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,Auto"),
|
||||
ColumnSpacing = 8
|
||||
};
|
||||
var leftStack = new StackPanel
|
||||
{
|
||||
Spacing = 3,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
cityText,
|
||||
dateText
|
||||
}
|
||||
};
|
||||
var timeStack = new StackPanel
|
||||
{
|
||||
Spacing = 2,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
timeText,
|
||||
offsetText
|
||||
}
|
||||
};
|
||||
row.Children.Add(leftStack);
|
||||
row.Children.Add(timeStack);
|
||||
row.Children.Add(upButton);
|
||||
row.Children.Add(downButton);
|
||||
row.Children.Add(removeButton);
|
||||
Grid.SetColumn(timeStack, 1);
|
||||
Grid.SetColumn(upButton, 2);
|
||||
Grid.SetColumn(downButton, 3);
|
||||
Grid.SetColumn(removeButton, 4);
|
||||
|
||||
WorldClockRowsPanel.Children.Add(new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.Parse("#0A000000")),
|
||||
CornerRadius = new CornerRadius(14),
|
||||
Padding = new Thickness(12, 10),
|
||||
Child = row
|
||||
});
|
||||
|
||||
_worldClockRows.Add(new WorldClockRowVisual
|
||||
{
|
||||
TimeZone = timeZone,
|
||||
TimeTextBlock = timeText,
|
||||
DateTextBlock = dateText,
|
||||
OffsetTextBlock = offsetText
|
||||
});
|
||||
}
|
||||
|
||||
private Button CreateIconButton(string text, string tooltip)
|
||||
{
|
||||
var button = new Button
|
||||
{
|
||||
Content = text,
|
||||
Classes = { "clock-icon-command" }
|
||||
};
|
||||
ToolTip.SetTip(button, tooltip);
|
||||
return button;
|
||||
}
|
||||
|
||||
private void MoveWorldClock(int index, int delta)
|
||||
{
|
||||
var nextIndex = index + delta;
|
||||
if (index < 0 || nextIndex < 0 || index >= _settings.WorldClockTimeZoneIds.Count || nextIndex >= _settings.WorldClockTimeZoneIds.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
(_settings.WorldClockTimeZoneIds[index], _settings.WorldClockTimeZoneIds[nextIndex]) =
|
||||
(_settings.WorldClockTimeZoneIds[nextIndex], _settings.WorldClockTimeZoneIds[index]);
|
||||
SaveWorldClockSettings();
|
||||
}
|
||||
|
||||
private void RemoveWorldClock(int index)
|
||||
{
|
||||
if (_settings.WorldClockTimeZoneIds.Count <= 1 || index < 0 || index >= _settings.WorldClockTimeZoneIds.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settings.WorldClockTimeZoneIds.RemoveAt(index);
|
||||
SaveWorldClockSettings();
|
||||
}
|
||||
|
||||
private void SaveWorldClockSettings()
|
||||
{
|
||||
_settings = ClockAirAppSettingsSnapshot.Normalize(_settings);
|
||||
_settingsStore.Save(_settings);
|
||||
RebuildWorldClockRows();
|
||||
UpdateWorldClock(DateTimeOffset.Now);
|
||||
}
|
||||
|
||||
private void OnStopwatchStartPauseClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
var now = DateTimeOffset.Now;
|
||||
if (_stopwatchState.IsRunning)
|
||||
{
|
||||
_stopwatchState.Pause(now);
|
||||
}
|
||||
else
|
||||
{
|
||||
_stopwatchState.StartOrResume(now);
|
||||
}
|
||||
|
||||
UpdateStopwatch(now);
|
||||
}
|
||||
|
||||
private void OnStopwatchLapClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
_ = _stopwatchState.AddLap(DateTimeOffset.Now);
|
||||
RebuildStopwatchLaps();
|
||||
}
|
||||
|
||||
private void OnStopwatchResetClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
_stopwatchState.Reset();
|
||||
RebuildStopwatchLaps();
|
||||
UpdateStopwatch(DateTimeOffset.Now);
|
||||
}
|
||||
|
||||
private void RebuildStopwatchLaps()
|
||||
{
|
||||
StopwatchLapsPanel.Children.Clear();
|
||||
for (var index = 0; index < _stopwatchState.Laps.Count; index++)
|
||||
{
|
||||
var lap = _stopwatchState.Laps[index];
|
||||
StopwatchLapsPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Lf("clockairapp.stopwatch.lap_format", "Lap {0} {1}", _stopwatchState.Laps.Count - index, ClockAirAppTimeFormatter.FormatDuration(lap, includeMilliseconds: true)),
|
||||
Foreground = TryGetBrush("AirAppSecondaryTextBrush", "#FF657080"),
|
||||
FontSize = 13
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTimerPresetClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button &&
|
||||
button.Tag is string minutesText &&
|
||||
int.TryParse(minutesText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var minutes))
|
||||
{
|
||||
SetTimerDuration(minutes);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTimerApplyClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (!int.TryParse(TimerMinutesTextBox.Text, NumberStyles.Integer, CultureInfo.CurrentCulture, out var minutes))
|
||||
{
|
||||
TimerStatusTextBlock.Text = L("clockairapp.timer.invalid", "Enter a valid minute value.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetTimerDuration(minutes);
|
||||
}
|
||||
|
||||
private void SetTimerDuration(int minutes)
|
||||
{
|
||||
minutes = Math.Clamp(minutes, 1, 24 * 60);
|
||||
TimerMinutesTextBox.Text = minutes.ToString(CultureInfo.CurrentCulture);
|
||||
_timerState.SetDuration(TimeSpan.FromMinutes(minutes));
|
||||
TimerStatusTextBlock.Text = Lf("clockairapp.timer.duration_status", "Duration {0}", ClockAirAppTimeFormatter.FormatDuration(_timerState.Duration));
|
||||
UpdateTimer(DateTimeOffset.Now);
|
||||
}
|
||||
|
||||
private void OnTimerStartPauseClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
var now = DateTimeOffset.Now;
|
||||
if (_timerState.IsRunning)
|
||||
{
|
||||
_timerState.Pause(now);
|
||||
}
|
||||
else
|
||||
{
|
||||
_timerState.StartOrResume(now);
|
||||
TimerStatusTextBlock.Text = string.Empty;
|
||||
}
|
||||
|
||||
UpdateTimer(now);
|
||||
}
|
||||
|
||||
private void OnTimerResetClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
_timerState.Reset();
|
||||
TimerStatusTextBlock.Text = Lf("clockairapp.timer.duration_status", "Duration {0}", ClockAirAppTimeFormatter.FormatDuration(_timerState.Duration));
|
||||
UpdateTimer(DateTimeOffset.Now);
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
_ = e;
|
||||
SaveSettingsFromControls(sender);
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = e;
|
||||
SaveSettingsFromControls(sender);
|
||||
}
|
||||
|
||||
private void SaveSettingsFromControls(object? sender)
|
||||
{
|
||||
_ = sender;
|
||||
if (_suppressSettingsEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settings.TimeFormatMode = TimeFormatComboBox.SelectedItem is ComboBoxItem timeFormatItem && timeFormatItem.Tag is string timeFormat
|
||||
? timeFormat
|
||||
: ClockAirAppTimeFormatMode.System;
|
||||
_settings.StartupTab = StartupTabComboBox.SelectedItem is ComboBoxItem startupItem && startupItem.Tag is string startupTab
|
||||
? startupTab
|
||||
: ClockAirAppTabIds.Last;
|
||||
_settings.ShowSeconds = ShowSecondsCheckBox.IsChecked == true;
|
||||
_settings.ActivateOnTimerFinished = ActivateOnTimerFinishedCheckBox.IsChecked == true;
|
||||
_settingsStore.Save(_settings);
|
||||
UpdateAll();
|
||||
}
|
||||
|
||||
private string ResolveRelativeDayLabel(int dayDelta)
|
||||
{
|
||||
if (dayDelta < 0)
|
||||
{
|
||||
return L("worldclock.widget.yesterday", "Yesterday");
|
||||
}
|
||||
|
||||
if (dayDelta > 0)
|
||||
{
|
||||
return L("worldclock.widget.tomorrow", "Tomorrow");
|
||||
}
|
||||
|
||||
return L("worldclock.widget.today", "Today");
|
||||
}
|
||||
|
||||
private IBrush TryGetBrush(string resourceKey, string fallbackColor)
|
||||
{
|
||||
return this.TryFindResource(resourceKey, out var value) && value is IBrush brush
|
||||
? brush
|
||||
: new SolidColorBrush(Color.Parse(fallbackColor));
|
||||
}
|
||||
|
||||
private string L(string key, string fallback)
|
||||
{
|
||||
return _localizationService.GetString(_languageCode, key, fallback);
|
||||
}
|
||||
|
||||
private string Lf(string key, string fallback, params object[] args)
|
||||
{
|
||||
return string.Format(_culture, L(key, fallback), args);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="..\LanMountainDesktop\Assets\Fonts\**" Link="Assets\Fonts\%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
<AvaloniaResource Include="..\LanMountainDesktop\Assets\logo_nightly.png" Link="Assets\logo_nightly.png" />
|
||||
<None Include="..\LanMountainDesktop\Localization\*.json"
|
||||
Link="Localization\%(Filename)%(Extension)"
|
||||
CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -5,6 +5,7 @@ using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Launcher.Models;
|
||||
using LanMountainDesktop.Launcher.Resources;
|
||||
using LanMountainDesktop.Launcher.Services;
|
||||
using LanMountainDesktop.Launcher.Services.AirApp;
|
||||
using LanMountainDesktop.Launcher.Services.Ipc;
|
||||
@@ -156,7 +157,7 @@ public partial class App : Application
|
||||
{
|
||||
Logger.Info("Preview command: error.");
|
||||
var errorWindow = new ErrorWindow();
|
||||
errorWindow.SetErrorMessage("[Preview] This is the launcher error window preview.");
|
||||
errorWindow.SetErrorMessage(Strings.Preview_ErrorMessage);
|
||||
errorWindow.Show();
|
||||
_ = WaitForWindowCloseAsync(desktop, errorWindow);
|
||||
return true;
|
||||
@@ -223,7 +224,7 @@ public partial class App : Application
|
||||
private async Task SimulateSplashPreviewAsync(IClassicDesktopStyleApplicationLifetime desktop, SplashWindow window)
|
||||
{
|
||||
var stages = new[] { "initializing", "update", "plugins", "launch", "ready" };
|
||||
var messages = new[] { "Initializing...", "Checking updates...", "Checking plugins...", "Launching host...", "Ready" };
|
||||
var messages = new[] { Strings.Preview_SplashInitializing, Strings.Preview_SplashCheckingUpdates, Strings.Preview_SplashCheckingPlugins, Strings.Preview_SplashLaunchingHost, Strings.Preview_SplashReady };
|
||||
var reporter = (ISplashStageReporter)window;
|
||||
|
||||
for (var i = 0; i < stages.Length; i++)
|
||||
@@ -242,7 +243,7 @@ public partial class App : Application
|
||||
|
||||
for (var i = 0; i < stages.Length; i++)
|
||||
{
|
||||
window.Report(stages[i], $"Processing {stages[i]}...", (i + 1) * 20);
|
||||
window.Report(stages[i], string.Format(Strings.Preview_UpdateProcessing, stages[i]), (i + 1) * 20);
|
||||
await Task.Delay(600).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -437,7 +438,7 @@ public partial class App : Application
|
||||
StartupAttemptRecord? activeCoordinatorAttempt)
|
||||
{
|
||||
var reporter = splashWindow as ISplashStageReporter;
|
||||
reporter?.Report("activation", "Connecting to the active launcher...");
|
||||
reporter?.Report("activation", Strings.Preview_ActivationConnecting);
|
||||
|
||||
if (activeCoordinatorAttempt is not null &&
|
||||
!string.IsNullOrWhiteSpace(activeCoordinatorAttempt.CoordinatorPipeName))
|
||||
@@ -794,7 +795,7 @@ public partial class App : Application
|
||||
|
||||
try
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() => window.Report("verify", "Verifying update...", 10));
|
||||
await Dispatcher.UIThread.InvokeAsync(() => window.Report("verify", Strings.Update_Verifying, 10));
|
||||
var updateResult = await updateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false);
|
||||
if (!updateResult.Success)
|
||||
{
|
||||
@@ -804,7 +805,7 @@ public partial class App : Application
|
||||
|
||||
if (success)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() => window.Report("plugins", "Applying plugin upgrades...", 60));
|
||||
await Dispatcher.UIThread.InvokeAsync(() => window.Report("plugins", Strings.Update_ApplyingPlugins, 60));
|
||||
var pluginsDir = context.GetOption("plugins-dir") ?? Path.Combine(appRoot, "plugins");
|
||||
var queueResult = pluginUpgrades.ApplyPendingUpgrades(pluginsDir);
|
||||
if (!queueResult.Success && queueResult.Code != "noop")
|
||||
@@ -815,7 +816,7 @@ public partial class App : Application
|
||||
|
||||
if (success)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() => window.Report("cleanup", "Cleaning up old deployments...", 90));
|
||||
await Dispatcher.UIThread.InvokeAsync(() => window.Report("cleanup", Strings.Update_CleaningUp, 90));
|
||||
deploymentLocator.CleanupOldDeployments(minVersionsToKeep: 3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
<TrimmerRootAssembly Include="Avalonia" />
|
||||
<TrimmerRootAssembly Include="Avalonia.Desktop" />
|
||||
|
||||
<TrimmerRootAssembly Include="LanMountainDesktop.Launcher" />
|
||||
|
||||
<!-- 保留动态序列化类型 -->
|
||||
<TrimmerRootAssembly Include="System.Text.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -34,6 +34,11 @@ public static class Program
|
||||
}
|
||||
|
||||
LauncherRuntimeContext.Current = commandContext;
|
||||
|
||||
var appRoot = Commands.ResolveAppRoot(commandContext);
|
||||
var languageCode = LanguagePreferenceService.ResolveLanguageCode(appRoot);
|
||||
LanguagePreferenceService.ApplyLanguage(languageCode);
|
||||
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||
return Environment.ExitCode;
|
||||
}
|
||||
|
||||
207
LanMountainDesktop.Launcher/Resources/Strings.cs
Normal file
207
LanMountainDesktop.Launcher/Resources/Strings.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using System.Globalization;
|
||||
using System.Resources;
|
||||
using System.Threading;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Resources;
|
||||
|
||||
public static class Strings
|
||||
{
|
||||
private static readonly ResourceManager ResourceManager =
|
||||
new("LanMountainDesktop.Launcher.Resources.Strings", typeof(Strings).Assembly);
|
||||
|
||||
public static string Splash_Title => ResourceManager.GetString(nameof(Splash_Title), Culture)!;
|
||||
public static string Splash_AppName => ResourceManager.GetString(nameof(Splash_AppName), Culture)!;
|
||||
public static string Splash_StatusInitializing => ResourceManager.GetString(nameof(Splash_StatusInitializing), Culture)!;
|
||||
public static string Splash_DebugPreview => ResourceManager.GetString(nameof(Splash_DebugPreview), Culture)!;
|
||||
public static string Error_Title => ResourceManager.GetString(nameof(Error_Title), Culture)!;
|
||||
public static string Error_TitleCannotConfirm => ResourceManager.GetString(nameof(Error_TitleCannotConfirm), Culture)!;
|
||||
public static string Error_MessageNotReached => ResourceManager.GetString(nameof(Error_MessageNotReached), Culture)!;
|
||||
public static string Error_SuggestionTitle => ResourceManager.GetString(nameof(Error_SuggestionTitle), Culture)!;
|
||||
public static string Error_SuggestionMessage => ResourceManager.GetString(nameof(Error_SuggestionMessage), Culture)!;
|
||||
public static string Error_DiagnosticHeader => ResourceManager.GetString(nameof(Error_DiagnosticHeader), Culture)!;
|
||||
public static string Error_ButtonOpenLogs => ResourceManager.GetString(nameof(Error_ButtonOpenLogs), Culture)!;
|
||||
public static string Error_ButtonCopy => ResourceManager.GetString(nameof(Error_ButtonCopy), Culture)!;
|
||||
public static string Error_ButtonWait => ResourceManager.GetString(nameof(Error_ButtonWait), Culture)!;
|
||||
public static string Error_ButtonExit => ResourceManager.GetString(nameof(Error_ButtonExit), Culture)!;
|
||||
public static string Error_ButtonRetry => ResourceManager.GetString(nameof(Error_ButtonRetry), Culture)!;
|
||||
public static string Error_ButtonActivate => ResourceManager.GetString(nameof(Error_ButtonActivate), Culture)!;
|
||||
public static string Error_DebugTitle => ResourceManager.GetString(nameof(Error_DebugTitle), Culture)!;
|
||||
public static string Error_HostNotFoundTitle => ResourceManager.GetString(nameof(Error_HostNotFoundTitle), Culture)!;
|
||||
public static string Error_HostNotFoundMessage => ResourceManager.GetString(nameof(Error_HostNotFoundMessage), Culture)!;
|
||||
public static string Error_GenericRetryMessage => ResourceManager.GetString(nameof(Error_GenericRetryMessage), Culture)!;
|
||||
public static string Error_GenericNoRetryMessage => ResourceManager.GetString(nameof(Error_GenericNoRetryMessage), Culture)!;
|
||||
public static string Error_PendingTitle => ResourceManager.GetString(nameof(Error_PendingTitle), Culture)!;
|
||||
public static string Error_PendingMessage => ResourceManager.GetString(nameof(Error_PendingMessage), Culture)!;
|
||||
public static string Error_PendingMessageWithPid => ResourceManager.GetString(nameof(Error_PendingMessageWithPid), Culture)!;
|
||||
public static string MultiInstance_Title => ResourceManager.GetString(nameof(MultiInstance_Title), Culture)!;
|
||||
public static string MultiInstance_AlreadyRunning => ResourceManager.GetString(nameof(MultiInstance_AlreadyRunning), Culture)!;
|
||||
public static string MultiInstance_AlreadyRunningMessage => ResourceManager.GetString(nameof(MultiInstance_AlreadyRunningMessage), Culture)!;
|
||||
public static string MultiInstance_RepeatedLaunchTitle => ResourceManager.GetString(nameof(MultiInstance_RepeatedLaunchTitle), Culture)!;
|
||||
public static string MultiInstance_RepeatedLaunchMessage => ResourceManager.GetString(nameof(MultiInstance_RepeatedLaunchMessage), Culture)!;
|
||||
public static string MultiInstance_NoSecondProcess => ResourceManager.GetString(nameof(MultiInstance_NoSecondProcess), Culture)!;
|
||||
public static string MultiInstance_ButtonCopy => ResourceManager.GetString(nameof(MultiInstance_ButtonCopy), Culture)!;
|
||||
public static string MultiInstance_ButtonClose => ResourceManager.GetString(nameof(MultiInstance_ButtonClose), Culture)!;
|
||||
public static string MultiInstance_ButtonOpenDesktop => ResourceManager.GetString(nameof(MultiInstance_ButtonOpenDesktop), Culture)!;
|
||||
public static string MultiInstance_DetailsFormat => ResourceManager.GetString(nameof(MultiInstance_DetailsFormat), Culture)!;
|
||||
public static string DataLocation_Title => ResourceManager.GetString(nameof(DataLocation_Title), Culture)!;
|
||||
public static string DataLocation_ChooseLocation => ResourceManager.GetString(nameof(DataLocation_ChooseLocation), Culture)!;
|
||||
public static string DataLocation_ChooseLocationDesc => ResourceManager.GetString(nameof(DataLocation_ChooseLocationDesc), Culture)!;
|
||||
public static string DataLocation_NotWritable => ResourceManager.GetString(nameof(DataLocation_NotWritable), Culture)!;
|
||||
public static string DataLocation_NotWritableDesc => ResourceManager.GetString(nameof(DataLocation_NotWritableDesc), Culture)!;
|
||||
public static string DataLocation_SystemProfile => ResourceManager.GetString(nameof(DataLocation_SystemProfile), Culture)!;
|
||||
public static string DataLocation_SystemProfileDesc => ResourceManager.GetString(nameof(DataLocation_SystemProfileDesc), Culture)!;
|
||||
public static string DataLocation_Portable => ResourceManager.GetString(nameof(DataLocation_Portable), Culture)!;
|
||||
public static string DataLocation_PortableDesc => ResourceManager.GetString(nameof(DataLocation_PortableDesc), Culture)!;
|
||||
public static string DataLocation_ButtonCancel => ResourceManager.GetString(nameof(DataLocation_ButtonCancel), Culture)!;
|
||||
public static string DataLocation_ButtonConfirm => ResourceManager.GetString(nameof(DataLocation_ButtonConfirm), Culture)!;
|
||||
public static string DataLocation_MigrateWarning => ResourceManager.GetString(nameof(DataLocation_MigrateWarning), Culture)!;
|
||||
public static string Loading_Title => ResourceManager.GetString(nameof(Loading_Title), Culture)!;
|
||||
public static string Loading_StartingDesktop => ResourceManager.GetString(nameof(Loading_StartingDesktop), Culture)!;
|
||||
public static string Loading_StatusInitializing => ResourceManager.GetString(nameof(Loading_StatusInitializing), Culture)!;
|
||||
public static string Loading_StatusPreparing => ResourceManager.GetString(nameof(Loading_StatusPreparing), Culture)!;
|
||||
public static string Loading_LoadingItems => ResourceManager.GetString(nameof(Loading_LoadingItems), Culture)!;
|
||||
public static string Loading_Done => ResourceManager.GetString(nameof(Loading_Done), Culture)!;
|
||||
public static string Loading_ErrorOccurred => ResourceManager.GetString(nameof(Loading_ErrorOccurred), Culture)!;
|
||||
public static string Loading_ButtonDetails => ResourceManager.GetString(nameof(Loading_ButtonDetails), Culture)!;
|
||||
public static string Loading_ButtonCancel => ResourceManager.GetString(nameof(Loading_ButtonCancel), Culture)!;
|
||||
public static string Loading_StageReady => ResourceManager.GetString(nameof(Loading_StageReady), Culture)!;
|
||||
public static string Loading_ItemPlugin => ResourceManager.GetString(nameof(Loading_ItemPlugin), Culture)!;
|
||||
public static string Loading_ItemComponent => ResourceManager.GetString(nameof(Loading_ItemComponent), Culture)!;
|
||||
public static string Loading_ItemResource => ResourceManager.GetString(nameof(Loading_ItemResource), Culture)!;
|
||||
public static string Loading_ItemData => ResourceManager.GetString(nameof(Loading_ItemData), Culture)!;
|
||||
public static string Loading_ItemDownload => ResourceManager.GetString(nameof(Loading_ItemDownload), Culture)!;
|
||||
public static string Loading_ItemProcess => ResourceManager.GetString(nameof(Loading_ItemProcess), Culture)!;
|
||||
public static string Loading_ItemComplete => ResourceManager.GetString(nameof(Loading_ItemComplete), Culture)!;
|
||||
public static string Loading_TypePlugin => ResourceManager.GetString(nameof(Loading_TypePlugin), Culture)!;
|
||||
public static string Loading_TypeComponent => ResourceManager.GetString(nameof(Loading_TypeComponent), Culture)!;
|
||||
public static string Loading_TypeResource => ResourceManager.GetString(nameof(Loading_TypeResource), Culture)!;
|
||||
public static string Loading_TypeData => ResourceManager.GetString(nameof(Loading_TypeData), Culture)!;
|
||||
public static string Loading_TypeNetwork => ResourceManager.GetString(nameof(Loading_TypeNetwork), Culture)!;
|
||||
public static string Loading_TypeSettings => ResourceManager.GetString(nameof(Loading_TypeSettings), Culture)!;
|
||||
public static string Loading_TypeSystem => ResourceManager.GetString(nameof(Loading_TypeSystem), Culture)!;
|
||||
public static string Loading_TypeOther => ResourceManager.GetString(nameof(Loading_TypeOther), Culture)!;
|
||||
public static string Update_Title => ResourceManager.GetString(nameof(Update_Title), Culture)!;
|
||||
public static string Update_AppName => ResourceManager.GetString(nameof(Update_AppName), Culture)!;
|
||||
public static string Update_StatusUpdate => ResourceManager.GetString(nameof(Update_StatusUpdate), Culture)!;
|
||||
public static string Update_StatusUpdating => ResourceManager.GetString(nameof(Update_StatusUpdating), Culture)!;
|
||||
public static string Update_Complete => ResourceManager.GetString(nameof(Update_Complete), Culture)!;
|
||||
public static string Update_Failed => ResourceManager.GetString(nameof(Update_Failed), Culture)!;
|
||||
public static string Update_FailedMessage => ResourceManager.GetString(nameof(Update_FailedMessage), Culture)!;
|
||||
public static string Update_DebugTitle => ResourceManager.GetString(nameof(Update_DebugTitle), Culture)!;
|
||||
public static string Update_DebugMessage => ResourceManager.GetString(nameof(Update_DebugMessage), Culture)!;
|
||||
public static string Update_Verifying => ResourceManager.GetString(nameof(Update_Verifying), Culture)!;
|
||||
public static string Update_ApplyingPlugins => ResourceManager.GetString(nameof(Update_ApplyingPlugins), Culture)!;
|
||||
public static string Update_CleaningUp => ResourceManager.GetString(nameof(Update_CleaningUp), Culture)!;
|
||||
public static string DebugDebug_Title => ResourceManager.GetString(nameof(DebugDebug_Title), Culture)!;
|
||||
public static string DebugDebug_SettingsTitle => ResourceManager.GetString(nameof(DebugDebug_SettingsTitle), Culture)!;
|
||||
public static string DebugDebug_DevMode => ResourceManager.GetString(nameof(DebugDebug_DevMode), Culture)!;
|
||||
public static string DebugDebug_DevModeDesc => ResourceManager.GetString(nameof(DebugDebug_DevModeDesc), Culture)!;
|
||||
public static string DebugDebug_On => ResourceManager.GetString(nameof(DebugDebug_On), Culture)!;
|
||||
public static string DebugDebug_Off => ResourceManager.GetString(nameof(DebugDebug_Off), Culture)!;
|
||||
public static string DebugDebug_AppPath => ResourceManager.GetString(nameof(DebugDebug_AppPath), Culture)!;
|
||||
public static string DebugDebug_NotSelected => ResourceManager.GetString(nameof(DebugDebug_NotSelected), Culture)!;
|
||||
public static string DebugDebug_Browse => ResourceManager.GetString(nameof(DebugDebug_Browse), Culture)!;
|
||||
public static string DebugDebug_Warning => ResourceManager.GetString(nameof(DebugDebug_Warning), Culture)!;
|
||||
public static string DebugDebug_ButtonCancel => ResourceManager.GetString(nameof(DebugDebug_ButtonCancel), Culture)!;
|
||||
public static string DebugDebug_ButtonOk => ResourceManager.GetString(nameof(DebugDebug_ButtonOk), Culture)!;
|
||||
public static string DebugDebug_SelectExeDialog => ResourceManager.GetString(nameof(DebugDebug_SelectExeDialog), Culture)!;
|
||||
public static string Oobe_Title => ResourceManager.GetString(nameof(Oobe_Title), Culture)!;
|
||||
public static string Oobe_WelcomeTitle => ResourceManager.GetString(nameof(Oobe_WelcomeTitle), Culture)!;
|
||||
public static string Oobe_WelcomeSubtitle => ResourceManager.GetString(nameof(Oobe_WelcomeSubtitle), Culture)!;
|
||||
public static string Oobe_ButtonGetStarted => ResourceManager.GetString(nameof(Oobe_ButtonGetStarted), Culture)!;
|
||||
public static string Oobe_AppearanceTitle => ResourceManager.GetString(nameof(Oobe_AppearanceTitle), Culture)!;
|
||||
public static string Oobe_AppearanceDesc => ResourceManager.GetString(nameof(Oobe_AppearanceDesc), Culture)!;
|
||||
public static string Oobe_AppearanceMode => ResourceManager.GetString(nameof(Oobe_AppearanceMode), Culture)!;
|
||||
public static string Oobe_LightMode => ResourceManager.GetString(nameof(Oobe_LightMode), Culture)!;
|
||||
public static string Oobe_DarkMode => ResourceManager.GetString(nameof(Oobe_DarkMode), Culture)!;
|
||||
public static string Oobe_ThemeColor => ResourceManager.GetString(nameof(Oobe_ThemeColor), Culture)!;
|
||||
public static string Oobe_MonetSource => ResourceManager.GetString(nameof(Oobe_MonetSource), Culture)!;
|
||||
public static string Oobe_MonetFromWallpaper => ResourceManager.GetString(nameof(Oobe_MonetFromWallpaper), Culture)!;
|
||||
public static string Oobe_MonetFromCustomImage => ResourceManager.GetString(nameof(Oobe_MonetFromCustomImage), Culture)!;
|
||||
public static string Oobe_MonetDisabled => ResourceManager.GetString(nameof(Oobe_MonetDisabled), Culture)!;
|
||||
public static string Oobe_DataLocationTitle => ResourceManager.GetString(nameof(Oobe_DataLocationTitle), Culture)!;
|
||||
public static string Oobe_SystemProfile => ResourceManager.GetString(nameof(Oobe_SystemProfile), Culture)!;
|
||||
public static string Oobe_SystemProfileDesc => ResourceManager.GetString(nameof(Oobe_SystemProfileDesc), Culture)!;
|
||||
public static string Oobe_Portable => ResourceManager.GetString(nameof(Oobe_Portable), Culture)!;
|
||||
public static string Oobe_PortableDesc => ResourceManager.GetString(nameof(Oobe_PortableDesc), Culture)!;
|
||||
public static string Oobe_NotWritable => ResourceManager.GetString(nameof(Oobe_NotWritable), Culture)!;
|
||||
public static string Oobe_NotWritableDesc => ResourceManager.GetString(nameof(Oobe_NotWritableDesc), Culture)!;
|
||||
public static string Oobe_StartupTitle => ResourceManager.GetString(nameof(Oobe_StartupTitle), Culture)!;
|
||||
public static string Oobe_ShowInTaskbar => ResourceManager.GetString(nameof(Oobe_ShowInTaskbar), Culture)!;
|
||||
public static string Oobe_SlideTransition => ResourceManager.GetString(nameof(Oobe_SlideTransition), Culture)!;
|
||||
public static string Oobe_FadeTransition => ResourceManager.GetString(nameof(Oobe_FadeTransition), Culture)!;
|
||||
public static string Oobe_FusedDesktop => ResourceManager.GetString(nameof(Oobe_FusedDesktop), Culture)!;
|
||||
public static string Oobe_AutoStart => ResourceManager.GetString(nameof(Oobe_AutoStart), Culture)!;
|
||||
public static string Oobe_PrivacyTitle => ResourceManager.GetString(nameof(Oobe_PrivacyTitle), Culture)!;
|
||||
public static string Oobe_CrashReports => ResourceManager.GetString(nameof(Oobe_CrashReports), Culture)!;
|
||||
public static string Oobe_UsageStats => ResourceManager.GetString(nameof(Oobe_UsageStats), Culture)!;
|
||||
public static string Oobe_PrivacyTrackingId => ResourceManager.GetString(nameof(Oobe_PrivacyTrackingId), Culture)!;
|
||||
public static string Oobe_Agree => ResourceManager.GetString(nameof(Oobe_Agree), Culture)!;
|
||||
public static string Oobe_PrivacyPolicyLink => ResourceManager.GetString(nameof(Oobe_PrivacyPolicyLink), Culture)!;
|
||||
public static string Oobe_ButtonBack => ResourceManager.GetString(nameof(Oobe_ButtonBack), Culture)!;
|
||||
public static string Oobe_ButtonNext => ResourceManager.GetString(nameof(Oobe_ButtonNext), Culture)!;
|
||||
public static string Oobe_CompleteTitle => ResourceManager.GetString(nameof(Oobe_CompleteTitle), Culture)!;
|
||||
public static string Oobe_CompleteSubtitle => ResourceManager.GetString(nameof(Oobe_CompleteSubtitle), Culture)!;
|
||||
public static string Oobe_MonetDesc => ResourceManager.GetString(nameof(Oobe_MonetDesc), Culture)!;
|
||||
public static string Oobe_MonetFromWallpaperDesc => ResourceManager.GetString(nameof(Oobe_MonetFromWallpaperDesc), Culture)!;
|
||||
public static string Oobe_MonetFromCustomImageDesc => ResourceManager.GetString(nameof(Oobe_MonetFromCustomImageDesc), Culture)!;
|
||||
public static string Oobe_MonetDisabledDesc => ResourceManager.GetString(nameof(Oobe_MonetDisabledDesc), Culture)!;
|
||||
public static string Oobe_DataLocationDesc => ResourceManager.GetString(nameof(Oobe_DataLocationDesc), Culture)!;
|
||||
public static string Oobe_StartupDesc => ResourceManager.GetString(nameof(Oobe_StartupDesc), Culture)!;
|
||||
public static string Oobe_ShowInTaskbarDesc => ResourceManager.GetString(nameof(Oobe_ShowInTaskbarDesc), Culture)!;
|
||||
public static string Oobe_SlideTransitionDesc => ResourceManager.GetString(nameof(Oobe_SlideTransitionDesc), Culture)!;
|
||||
public static string Oobe_FadeTransitionDesc => ResourceManager.GetString(nameof(Oobe_FadeTransitionDesc), Culture)!;
|
||||
public static string Oobe_FusedDesktopDesc => ResourceManager.GetString(nameof(Oobe_FusedDesktopDesc), Culture)!;
|
||||
public static string Oobe_AutoStartDesc => ResourceManager.GetString(nameof(Oobe_AutoStartDesc), Culture)!;
|
||||
public static string Oobe_AutoStartDescNonWindows => ResourceManager.GetString(nameof(Oobe_AutoStartDescNonWindows), Culture)!;
|
||||
public static string Oobe_PrivacyDesc => ResourceManager.GetString(nameof(Oobe_PrivacyDesc), Culture)!;
|
||||
public static string Oobe_CrashReportsDesc => ResourceManager.GetString(nameof(Oobe_CrashReportsDesc), Culture)!;
|
||||
public static string Oobe_UsageStatsDesc => ResourceManager.GetString(nameof(Oobe_UsageStatsDesc), Culture)!;
|
||||
public static string Oobe_PrivacyTrackingIdDesc => ResourceManager.GetString(nameof(Oobe_PrivacyTrackingIdDesc), Culture)!;
|
||||
public static string Oobe_PrivacyAgreementNote => ResourceManager.GetString(nameof(Oobe_PrivacyAgreementNote), Culture)!;
|
||||
public static string Oobe_TypingAppName => ResourceManager.GetString(nameof(Oobe_TypingAppName), Culture)!;
|
||||
public static string Oobe_TypingNextGen => ResourceManager.GetString(nameof(Oobe_TypingNextGen), Culture)!;
|
||||
public static string Oobe_TypingDashboard => ResourceManager.GetString(nameof(Oobe_TypingDashboard), Culture)!;
|
||||
public static string Oobe_MigrationDetected => ResourceManager.GetString(nameof(Oobe_MigrationDetected), Culture)!;
|
||||
public static string Migration_Title => ResourceManager.GetString(nameof(Migration_Title), Culture)!;
|
||||
public static string Migration_DetectedOldVersion => ResourceManager.GetString(nameof(Migration_DetectedOldVersion), Culture)!;
|
||||
public static string Migration_DetectedDesc => ResourceManager.GetString(nameof(Migration_DetectedDesc), Culture)!;
|
||||
public static string Migration_Version => ResourceManager.GetString(nameof(Migration_Version), Culture)!;
|
||||
public static string Migration_Location => ResourceManager.GetString(nameof(Migration_Location), Culture)!;
|
||||
public static string Migration_Type => ResourceManager.GetString(nameof(Migration_Type), Culture)!;
|
||||
public static string Migration_Installed => ResourceManager.GetString(nameof(Migration_Installed), Culture)!;
|
||||
public static string Migration_UninstallNote => ResourceManager.GetString(nameof(Migration_UninstallNote), Culture)!;
|
||||
public static string Migration_ButtonViewLocation => ResourceManager.GetString(nameof(Migration_ButtonViewLocation), Culture)!;
|
||||
public static string Migration_ButtonSkip => ResourceManager.GetString(nameof(Migration_ButtonSkip), Culture)!;
|
||||
public static string Migration_ButtonUninstall => ResourceManager.GetString(nameof(Migration_ButtonUninstall), Culture)!;
|
||||
public static string Migration_Portable => ResourceManager.GetString(nameof(Migration_Portable), Culture)!;
|
||||
public static string Migration_Unknown => ResourceManager.GetString(nameof(Migration_Unknown), Culture)!;
|
||||
public static string Migration_DetectedDescFormat => ResourceManager.GetString(nameof(Migration_DetectedDescFormat), Culture)!;
|
||||
public static string Privacy_Title => ResourceManager.GetString(nameof(Privacy_Title), Culture)!;
|
||||
public static string Privacy_Header => ResourceManager.GetString(nameof(Privacy_Header), Culture)!;
|
||||
public static string Privacy_Description => ResourceManager.GetString(nameof(Privacy_Description), Culture)!;
|
||||
public static string Privacy_ButtonClose => ResourceManager.GetString(nameof(Privacy_ButtonClose), Culture)!;
|
||||
public static string DevDebug_Title => ResourceManager.GetString(nameof(DevDebug_Title), Culture)!;
|
||||
public static string DevDebug_Splash => ResourceManager.GetString(nameof(DevDebug_Splash), Culture)!;
|
||||
public static string DevDebug_Error => ResourceManager.GetString(nameof(DevDebug_Error), Culture)!;
|
||||
public static string DevDebug_Update => ResourceManager.GetString(nameof(DevDebug_Update), Culture)!;
|
||||
public static string DevDebug_Oobe => ResourceManager.GetString(nameof(DevDebug_Oobe), Culture)!;
|
||||
public static string DevDebug_DataLocation => ResourceManager.GetString(nameof(DevDebug_DataLocation), Culture)!;
|
||||
public static string DevDebug_EnableFeature => ResourceManager.GetString(nameof(DevDebug_EnableFeature), Culture)!;
|
||||
public static string DevDebug_Open => ResourceManager.GetString(nameof(DevDebug_Open), Culture)!;
|
||||
public static string DevDebug_SetAllViewMode => ResourceManager.GetString(nameof(DevDebug_SetAllViewMode), Culture)!;
|
||||
public static string DevDebug_SetAllFunctionMode => ResourceManager.GetString(nameof(DevDebug_SetAllFunctionMode), Culture)!;
|
||||
public static string DevDebug_Close => ResourceManager.GetString(nameof(DevDebug_Close), Culture)!;
|
||||
public static string Coordinator_SlowDeviceMessage => ResourceManager.GetString(nameof(Coordinator_SlowDeviceMessage), Culture)!;
|
||||
public static string Coordinator_RunningHostMessage => ResourceManager.GetString(nameof(Coordinator_RunningHostMessage), Culture)!;
|
||||
public static string Preview_SplashInitializing => ResourceManager.GetString(nameof(Preview_SplashInitializing), Culture)!;
|
||||
public static string Preview_SplashCheckingUpdates => ResourceManager.GetString(nameof(Preview_SplashCheckingUpdates), Culture)!;
|
||||
public static string Preview_SplashCheckingPlugins => ResourceManager.GetString(nameof(Preview_SplashCheckingPlugins), Culture)!;
|
||||
public static string Preview_SplashLaunchingHost => ResourceManager.GetString(nameof(Preview_SplashLaunchingHost), Culture)!;
|
||||
public static string Preview_SplashReady => ResourceManager.GetString(nameof(Preview_SplashReady), Culture)!;
|
||||
public static string Preview_ErrorMessage => ResourceManager.GetString(nameof(Preview_ErrorMessage), Culture)!;
|
||||
public static string Preview_UpdateProcessing => ResourceManager.GetString(nameof(Preview_UpdateProcessing), Culture)!;
|
||||
public static string Preview_ActivationConnecting => ResourceManager.GetString(nameof(Preview_ActivationConnecting), Culture)!;
|
||||
|
||||
private static CultureInfo? Culture => CultureInfo.CurrentUICulture;
|
||||
}
|
||||
219
LanMountainDesktop.Launcher/Resources/Strings.en-US.resx
Normal file
219
LanMountainDesktop.Launcher/Resources/Strings.en-US.resx
Normal file
@@ -0,0 +1,219 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
<xsd:attribute name="type" type="xsd:string" use="optional" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" use="optional" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
|
||||
<resheader name="version"><value>2.0</value></resheader>
|
||||
<resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms</value></resheader>
|
||||
<resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms</value></resheader>
|
||||
<data name="Splash_Title" xml:space="preserve"><value>LanMountain Desktop</value></data>
|
||||
<data name="Splash_AppName" xml:space="preserve"><value>LanMountain Desktop</value></data>
|
||||
<data name="Splash_StatusInitializing" xml:space="preserve"><value>Initializing...</value></data>
|
||||
<data name="Splash_DebugPreview" xml:space="preserve"><value>[Debug Mode] Splash Preview</value></data>
|
||||
<data name="Error_Title" xml:space="preserve"><value>LanMountain Desktop</value></data>
|
||||
<data name="Error_TitleCannotConfirm" xml:space="preserve"><value>Launcher could not confirm startup</value></data>
|
||||
<data name="Error_MessageNotReached" xml:space="preserve"><value>LanMountain Desktop did not reach the expected startup state.</value></data>
|
||||
<data name="Error_SuggestionTitle" xml:space="preserve"><value>Startup recovery</value></data>
|
||||
<data name="Error_SuggestionMessage" xml:space="preserve"><value>You can inspect logs, wait for the current process, or activate the running desktop instance.</value></data>
|
||||
<data name="Error_DiagnosticHeader" xml:space="preserve"><value>Diagnostic details</value></data>
|
||||
<data name="Error_ButtonOpenLogs" xml:space="preserve"><value>Open Logs</value></data>
|
||||
<data name="Error_ButtonCopy" xml:space="preserve"><value>Copy</value></data>
|
||||
<data name="Error_ButtonWait" xml:space="preserve"><value>Wait</value></data>
|
||||
<data name="Error_ButtonExit" xml:space="preserve"><value>Exit</value></data>
|
||||
<data name="Error_ButtonRetry" xml:space="preserve"><value>Retry</value></data>
|
||||
<data name="Error_ButtonActivate" xml:space="preserve"><value>Activate</value></data>
|
||||
<data name="Error_DebugTitle" xml:space="preserve"><value>[Debug] Launcher error</value></data>
|
||||
<data name="Error_HostNotFoundTitle" xml:space="preserve"><value>Launcher could not find the desktop executable</value></data>
|
||||
<data name="Error_HostNotFoundMessage" xml:space="preserve"><value>Pick another executable in debug mode, inspect logs, or retry after fixing the deployment path.</value></data>
|
||||
<data name="Error_GenericRetryMessage" xml:space="preserve"><value>Inspect logs, then retry once the previous startup attempt has fully finished.</value></data>
|
||||
<data name="Error_GenericNoRetryMessage" xml:space="preserve"><value>Inspect logs or exit. Launcher will avoid creating another desktop process while the old one is still running.</value></data>
|
||||
<data name="Error_PendingTitle" xml:space="preserve"><value>Startup is still pending</value></data>
|
||||
<data name="Error_PendingMessage" xml:space="preserve"><value>The desktop process is still running, so Launcher will not start a second instance.</value></data>
|
||||
<data name="Error_PendingMessageWithPid" xml:space="preserve"><value>The desktop process is still running, so Launcher will not start a second instance. Current host PID: {0}.</value></data>
|
||||
<data name="MultiInstance_Title" xml:space="preserve"><value>LanMountain Desktop</value></data>
|
||||
<data name="MultiInstance_AlreadyRunning" xml:space="preserve"><value>LanMountain Desktop is already running</value></data>
|
||||
<data name="MultiInstance_AlreadyRunningMessage" xml:space="preserve"><value>Launcher found an existing desktop instance and did not start another process.</value></data>
|
||||
<data name="MultiInstance_RepeatedLaunchTitle" xml:space="preserve"><value>Repeated launch</value></data>
|
||||
<data name="MultiInstance_RepeatedLaunchMessage" xml:space="preserve"><value>Your current setting is to show this prompt without opening the desktop automatically.</value></data>
|
||||
<data name="MultiInstance_NoSecondProcess" xml:space="preserve"><value>No second Host process was created.</value></data>
|
||||
<data name="MultiInstance_ButtonCopy" xml:space="preserve"><value>Copy</value></data>
|
||||
<data name="MultiInstance_ButtonClose" xml:space="preserve"><value>Close</value></data>
|
||||
<data name="MultiInstance_ButtonOpenDesktop" xml:space="preserve"><value>Open desktop</value></data>
|
||||
<data name="MultiInstance_DetailsFormat" xml:space="preserve"><value>Existing host PID: {0} Shell state: {1} No second Host process was created.</value></data>
|
||||
<data name="DataLocation_Title" xml:space="preserve"><value>Choose Data Location</value></data>
|
||||
<data name="DataLocation_ChooseLocation" xml:space="preserve"><value>Choose Data Location</value></data>
|
||||
<data name="DataLocation_ChooseLocationDesc" xml:space="preserve"><value>Choose where launcher and desktop data should be stored. You can change this later in settings.</value></data>
|
||||
<data name="DataLocation_NotWritable" xml:space="preserve"><value>App folder is not writable</value></data>
|
||||
<data name="DataLocation_NotWritableDesc" xml:space="preserve"><value>The current install directory requires elevated permissions. Data will be stored in the system user profile instead.</value></data>
|
||||
<data name="DataLocation_SystemProfile" xml:space="preserve"><value>Store in the system user profile (Recommended)</value></data>
|
||||
<data name="DataLocation_SystemProfileDesc" xml:space="preserve"><value>Data stays tied to the current Windows user and remains intact across app reinstalls and updates.</value></data>
|
||||
<data name="DataLocation_Portable" xml:space="preserve"><value>Store next to the app</value></data>
|
||||
<data name="DataLocation_PortableDesc" xml:space="preserve"><value>Useful for portable installs. The whole app folder can be moved to another machine together with its data.</value></data>
|
||||
<data name="DataLocation_ButtonCancel" xml:space="preserve"><value>Cancel</value></data>
|
||||
<data name="DataLocation_ButtonConfirm" xml:space="preserve"><value>Confirm</value></data>
|
||||
<data name="DataLocation_MigrateWarning" xml:space="preserve"><value>Existing system data was detected. Choosing portable mode will migrate the current data automatically.</value></data>
|
||||
<data name="Loading_Title" xml:space="preserve"><value>LanMountain Desktop - Loading Details</value></data>
|
||||
<data name="Loading_StartingDesktop" xml:space="preserve"><value>Starting LanMountain Desktop</value></data>
|
||||
<data name="Loading_StatusInitializing" xml:space="preserve"><value>Initializing...</value></data>
|
||||
<data name="Loading_StatusPreparing" xml:space="preserve"><value>Preparing components</value></data>
|
||||
<data name="Loading_LoadingItems" xml:space="preserve"><value>Loading Items</value></data>
|
||||
<data name="Loading_Done" xml:space="preserve"><value>Done</value></data>
|
||||
<data name="Loading_ErrorOccurred" xml:space="preserve"><value>An error occurred while loading.</value></data>
|
||||
<data name="Loading_ButtonDetails" xml:space="preserve"><value>Details</value></data>
|
||||
<data name="Loading_ButtonCancel" xml:space="preserve"><value>Cancel</value></data>
|
||||
<data name="Loading_StageReady" xml:space="preserve"><value>Ready</value></data>
|
||||
<data name="Loading_ItemPlugin" xml:space="preserve"><value>Loading plugins...</value></data>
|
||||
<data name="Loading_ItemComponent" xml:space="preserve"><value>Loading components...</value></data>
|
||||
<data name="Loading_ItemResource" xml:space="preserve"><value>Loading resources...</value></data>
|
||||
<data name="Loading_ItemData" xml:space="preserve"><value>Loading data...</value></data>
|
||||
<data name="Loading_ItemDownload" xml:space="preserve"><value>Downloading...</value></data>
|
||||
<data name="Loading_ItemProcess" xml:space="preserve"><value>Processing...</value></data>
|
||||
<data name="Loading_ItemComplete" xml:space="preserve"><value>Done</value></data>
|
||||
<data name="Loading_TypePlugin" xml:space="preserve"><value>Plugin</value></data>
|
||||
<data name="Loading_TypeComponent" xml:space="preserve"><value>Component</value></data>
|
||||
<data name="Loading_TypeResource" xml:space="preserve"><value>Resource</value></data>
|
||||
<data name="Loading_TypeData" xml:space="preserve"><value>Data</value></data>
|
||||
<data name="Loading_TypeNetwork" xml:space="preserve"><value>Network</value></data>
|
||||
<data name="Loading_TypeSettings" xml:space="preserve"><value>Settings</value></data>
|
||||
<data name="Loading_TypeSystem" xml:space="preserve"><value>System</value></data>
|
||||
<data name="Loading_TypeOther" xml:space="preserve"><value>Other</value></data>
|
||||
<data name="Update_Title" xml:space="preserve"><value>LanMountain Desktop - Update</value></data>
|
||||
<data name="Update_AppName" xml:space="preserve"><value>LanMountain Desktop</value></data>
|
||||
<data name="Update_StatusUpdate" xml:space="preserve"><value>Update</value></data>
|
||||
<data name="Update_StatusUpdating" xml:space="preserve"><value>Updating, please wait...</value></data>
|
||||
<data name="Update_Complete" xml:space="preserve"><value>Update complete</value></data>
|
||||
<data name="Update_Failed" xml:space="preserve"><value>Update failed</value></data>
|
||||
<data name="Update_FailedMessage" xml:space="preserve"><value>An error occurred during the update</value></data>
|
||||
<data name="Update_DebugTitle" xml:space="preserve"><value>[Debug Mode] Update Page</value></data>
|
||||
<data name="Update_DebugMessage" xml:space="preserve"><value>Preview update progress interface</value></data>
|
||||
<data name="Update_Verifying" xml:space="preserve"><value>Verifying update...</value></data>
|
||||
<data name="Update_ApplyingPlugins" xml:space="preserve"><value>Applying plugin upgrades...</value></data>
|
||||
<data name="Update_CleaningUp" xml:space="preserve"><value>Cleaning up old deployments...</value></data>
|
||||
<data name="DebugDebug_Title" xml:space="preserve"><value>Debug Mode</value></data>
|
||||
<data name="DebugDebug_SettingsTitle" xml:space="preserve"><value>Debug Settings</value></data>
|
||||
<data name="DebugDebug_DevMode" xml:space="preserve"><value>Developer Mode</value></data>
|
||||
<data name="DebugDebug_DevModeDesc" xml:space="preserve"><value>Automatically scan dev directories when enabled</value></data>
|
||||
<data name="DebugDebug_On" xml:space="preserve"><value>On</value></data>
|
||||
<data name="DebugDebug_Off" xml:space="preserve"><value>Off</value></data>
|
||||
<data name="DebugDebug_AppPath" xml:space="preserve"><value>App Path</value></data>
|
||||
<data name="DebugDebug_NotSelected" xml:space="preserve"><value>Not selected</value></data>
|
||||
<data name="DebugDebug_Browse" xml:space="preserve"><value>Browse...</value></data>
|
||||
<data name="DebugDebug_Warning" xml:space="preserve"><value>This feature is for developers only</value></data>
|
||||
<data name="DebugDebug_ButtonCancel" xml:space="preserve"><value>Cancel</value></data>
|
||||
<data name="DebugDebug_ButtonOk" xml:space="preserve"><value>OK</value></data>
|
||||
<data name="DebugDebug_SelectExeDialog" xml:space="preserve"><value>Select LanMountainDesktop host executable</value></data>
|
||||
<data name="Oobe_Title" xml:space="preserve"><value>Welcome to LanMountain Desktop</value></data>
|
||||
<data name="Oobe_WelcomeTitle" xml:space="preserve"><value>Welcome to LanMountain Desktop</value></data>
|
||||
<data name="Oobe_WelcomeSubtitle" xml:space="preserve"><value>Your desktop, more than one side</value></data>
|
||||
<data name="Oobe_ButtonGetStarted" xml:space="preserve"><value>Get Started</value></data>
|
||||
<data name="Oobe_AppearanceTitle" xml:space="preserve"><value>Personalize Your Desktop</value></data>
|
||||
<data name="Oobe_AppearanceDesc" xml:space="preserve"><value>Choose your preferred theme style. You can change it anytime in settings.</value></data>
|
||||
<data name="Oobe_AppearanceMode" xml:space="preserve"><value>Appearance Mode</value></data>
|
||||
<data name="Oobe_LightMode" xml:space="preserve"><value>Light Mode</value></data>
|
||||
<data name="Oobe_DarkMode" xml:space="preserve"><value>Dark Mode</value></data>
|
||||
<data name="Oobe_ThemeColor" xml:space="preserve"><value>Theme Color</value></data>
|
||||
<data name="Oobe_MonetSource" xml:space="preserve"><value>Monet Color Source</value></data>
|
||||
<data name="Oobe_MonetFromWallpaper" xml:space="preserve"><value>Extract from wallpaper</value></data>
|
||||
<data name="Oobe_MonetFromCustomImage" xml:space="preserve"><value>Extract from custom image</value></data>
|
||||
<data name="Oobe_MonetDisabled" xml:space="preserve"><value>Don't use Monet colors</value></data>
|
||||
<data name="Oobe_DataLocationTitle" xml:space="preserve"><value>Choose Data Location</value></data>
|
||||
<data name="Oobe_SystemProfile" xml:space="preserve"><value>Store in the system user profile (Recommended)</value></data>
|
||||
<data name="Oobe_SystemProfileDesc" xml:space="preserve"><value>Data stays tied to the current Windows user and remains intact across app reinstalls and updates.</value></data>
|
||||
<data name="Oobe_Portable" xml:space="preserve"><value>Store next to the app</value></data>
|
||||
<data name="Oobe_PortableDesc" xml:space="preserve"><value>Useful for portable installs. The whole app folder can be moved to another machine together with its data.</value></data>
|
||||
<data name="Oobe_NotWritable" xml:space="preserve"><value>Cannot save to app directory</value></data>
|
||||
<data name="Oobe_NotWritableDesc" xml:space="preserve"><value>The current install directory requires elevated permissions. Data will be stored in the system user profile instead.</value></data>
|
||||
<data name="Oobe_StartupTitle" xml:space="preserve"><value>Startup & Display</value></data>
|
||||
<data name="Oobe_ShowInTaskbar" xml:space="preserve"><value>Show main desktop window in taskbar</value></data>
|
||||
<data name="Oobe_SlideTransition" xml:space="preserve"><value>Show main window with slide transition</value></data>
|
||||
<data name="Oobe_FadeTransition" xml:space="preserve"><value>Use fade-in transition on startup</value></data>
|
||||
<data name="Oobe_FusedDesktop" xml:space="preserve"><value>Fused desktop with swipe gesture</value></data>
|
||||
<data name="Oobe_AutoStart" xml:space="preserve"><value>Automatically start LanMountain Desktop on Windows login</value></data>
|
||||
<data name="Oobe_PrivacyTitle" xml:space="preserve"><value>Information & Privacy</value></data>
|
||||
<data name="Oobe_CrashReports" xml:space="preserve"><value>Send anonymous crash reports</value></data>
|
||||
<data name="Oobe_UsageStats" xml:space="preserve"><value>Send anonymous usage statistics</value></data>
|
||||
<data name="Oobe_PrivacyTrackingId" xml:space="preserve"><value>Privacy Tracking ID</value></data>
|
||||
<data name="Oobe_Agree" xml:space="preserve"><value>Agree</value></data>
|
||||
<data name="Oobe_PrivacyPolicyLink" xml:space="preserve"><value>LanMountain Desktop Telemetry Privacy Data Collection Agreement</value></data>
|
||||
<data name="Oobe_ButtonBack" xml:space="preserve"><value>Back</value></data>
|
||||
<data name="Oobe_ButtonNext" xml:space="preserve"><value>Next</value></data>
|
||||
<data name="Oobe_CompleteTitle" xml:space="preserve"><value>Welcome to LanMountain Desktop</value></data>
|
||||
<data name="Oobe_CompleteSubtitle" xml:space="preserve"><value>Your desktop, more than one side</value></data>
|
||||
<data name="Oobe_MonetDesc" xml:space="preserve"><value>Automatically extract theme colors from your wallpaper for a seamless desktop experience</value></data>
|
||||
<data name="Oobe_MonetFromWallpaperDesc" xml:space="preserve"><value>Analyze current wallpaper colors to generate a theme</value></data>
|
||||
<data name="Oobe_MonetFromCustomImageDesc" xml:space="preserve"><value>Choose an image as the color source</value></data>
|
||||
<data name="Oobe_MonetDisabledDesc" xml:space="preserve"><value>Use a fixed preset theme color</value></data>
|
||||
<data name="Oobe_DataLocationDesc" xml:space="preserve"><value>Decide where to store app data. You can change this anytime in settings.</value></data>
|
||||
<data name="Oobe_StartupDesc" xml:space="preserve"><value>These options can be changed anytime in the desktop app Settings. Slide-in entrance is only available on Windows.</value></data>
|
||||
<data name="Oobe_ShowInTaskbarDesc" xml:space="preserve"><value>When enabled, a taskbar entry remains when minimized; when disabled, rely more on the tray icon.</value></data>
|
||||
<data name="Oobe_SlideTransitionDesc" xml:space="preserve"><value>Slide in from screen edge; mutually exclusive with fade-in.</value></data>
|
||||
<data name="Oobe_FadeTransitionDesc" xml:space="preserve"><value>Recommended when slide-in is not enabled.</value></data>
|
||||
<data name="Oobe_FusedDesktopDesc" xml:space="preserve"><value>Enable fused desktop and three-finger swipe gesture for edge pop-in and related experimental features (same as developer options in Settings).</value></data>
|
||||
<data name="Oobe_AutoStartDesc" xml:space="preserve"><value>Register this launcher as the current user's startup item (same registry entry as the optional installer task).</value></data>
|
||||
<data name="Oobe_AutoStartDescNonWindows" xml:space="preserve"><value>Only the preference is saved on this platform; use the system's app auto-start settings for actual auto-start behavior.</value></data>
|
||||
<data name="Oobe_PrivacyDesc" xml:space="preserve"><value>Choose whether to participate in the telemetry program and review the privacy policy</value></data>
|
||||
<data name="Oobe_CrashReportsDesc" xml:space="preserve"><value>Help improve app stability; no personal identity information is included</value></data>
|
||||
<data name="Oobe_UsageStatsDesc" xml:space="preserve"><value>Help understand feature usage and optimize product experience</value></data>
|
||||
<data name="Oobe_PrivacyTrackingIdDesc" xml:space="preserve"><value>This ID is used to anonymously identify your device and does not contain any personal information</value></data>
|
||||
<data name="Oobe_PrivacyAgreementNote" xml:space="preserve"><value>You must read and agree to the privacy policy before enabling telemetry features. Telemetry data is used solely to improve app stability and optimize product experience, and does not contain any personal identity information.</value></data>
|
||||
<data name="Oobe_TypingAppName" xml:space="preserve"><value>LanMountain Desktop</value></data>
|
||||
<data name="Oobe_TypingNextGen" xml:space="preserve"><value>Next Gen</value></data>
|
||||
<data name="Oobe_TypingDashboard" xml:space="preserve"><value>Interactive Dashboard</value></data>
|
||||
<data name="Oobe_MigrationDetected" xml:space="preserve"><value>Existing data detected. It will be migrated automatically when portable mode is selected.</value></data>
|
||||
<data name="Migration_Title" xml:space="preserve"><value>LanMountain Desktop - Version Migration</value></data>
|
||||
<data name="Migration_DetectedOldVersion" xml:space="preserve"><value>Old version detected</value></data>
|
||||
<data name="Migration_DetectedDesc" xml:space="preserve"><value>An older version of LanMountain Desktop (0.8.4) was detected on your system. Uninstalling it is recommended to avoid conflicts.</value></data>
|
||||
<data name="Migration_Version" xml:space="preserve"><value>Version: </value></data>
|
||||
<data name="Migration_Location" xml:space="preserve"><value>Location: </value></data>
|
||||
<data name="Migration_Type" xml:space="preserve"><value>Type: </value></data>
|
||||
<data name="Migration_Installed" xml:space="preserve"><value>Installed</value></data>
|
||||
<data name="Migration_UninstallNote" xml:space="preserve"><value>Uninstalling the old version will not affect the new version. Your personal data will be preserved.</value></data>
|
||||
<data name="Migration_ButtonViewLocation" xml:space="preserve"><value>View Location</value></data>
|
||||
<data name="Migration_ButtonSkip" xml:space="preserve"><value>Skip for now</value></data>
|
||||
<data name="Migration_ButtonUninstall" xml:space="preserve"><value>Uninstall Old Version</value></data>
|
||||
<data name="Migration_Portable" xml:space="preserve"><value>Portable</value></data>
|
||||
<data name="Migration_Unknown" xml:space="preserve"><value>Unknown</value></data>
|
||||
<data name="Migration_DetectedDescFormat" xml:space="preserve"><value>An older version of LanMountain Desktop ({0}) was detected on your system. The new version uses a completely new architecture. Uninstalling the old version is recommended for a better experience.</value></data>
|
||||
<data name="Privacy_Title" xml:space="preserve"><value>LanMountain Desktop Telemetry Privacy Data Collection Agreement</value></data>
|
||||
<data name="Privacy_Header" xml:space="preserve"><value>LanMountain Desktop Telemetry Privacy Data Collection Agreement</value></data>
|
||||
<data name="Privacy_Description" xml:space="preserve"><value>Please read the following agreement carefully to understand how we collect, use, and protect your data</value></data>
|
||||
<data name="Privacy_ButtonClose" xml:space="preserve"><value>Close</value></data>
|
||||
<data name="DevDebug_Title" xml:space="preserve"><value>Developer Debug Window</value></data>
|
||||
<data name="DevDebug_Splash" xml:space="preserve"><value>Splash Screen</value></data>
|
||||
<data name="DevDebug_Error" xml:space="preserve"><value>Error Page</value></data>
|
||||
<data name="DevDebug_Update" xml:space="preserve"><value>Update Page</value></data>
|
||||
<data name="DevDebug_Oobe" xml:space="preserve"><value>OOBE Page</value></data>
|
||||
<data name="DevDebug_DataLocation" xml:space="preserve"><value>Data Location</value></data>
|
||||
<data name="DevDebug_EnableFeature" xml:space="preserve"><value>Enable Feature</value></data>
|
||||
<data name="DevDebug_Open" xml:space="preserve"><value>Open</value></data>
|
||||
<data name="DevDebug_SetAllViewMode" xml:space="preserve"><value>Set All to View Mode</value></data>
|
||||
<data name="DevDebug_SetAllFunctionMode" xml:space="preserve"><value>Set All to Function Mode</value></data>
|
||||
<data name="DevDebug_Close" xml:space="preserve"><value>Close</value></data>
|
||||
<data name="Coordinator_SlowDeviceMessage" xml:space="preserve"><value>The device is slow and still starting up. Please wait.</value></data>
|
||||
<data name="Coordinator_RunningHostMessage" xml:space="preserve"><value>The desktop process is still running. Launcher will continue waiting and will not start again.</value></data>
|
||||
<data name="Preview_SplashInitializing" xml:space="preserve"><value>Initializing...</value></data>
|
||||
<data name="Preview_SplashCheckingUpdates" xml:space="preserve"><value>Checking updates...</value></data>
|
||||
<data name="Preview_SplashCheckingPlugins" xml:space="preserve"><value>Checking plugins...</value></data>
|
||||
<data name="Preview_SplashLaunchingHost" xml:space="preserve"><value>Launching host...</value></data>
|
||||
<data name="Preview_SplashReady" xml:space="preserve"><value>Ready</value></data>
|
||||
<data name="Preview_ErrorMessage" xml:space="preserve"><value>[Preview] This is the launcher error window preview.</value></data>
|
||||
<data name="Preview_UpdateProcessing" xml:space="preserve"><value>Processing {0}...</value></data>
|
||||
<data name="Preview_ActivationConnecting" xml:space="preserve"><value>Connecting to the active launcher...</value></data>
|
||||
</root>
|
||||
219
LanMountainDesktop.Launcher/Resources/Strings.ja-JP.resx
Normal file
219
LanMountainDesktop.Launcher/Resources/Strings.ja-JP.resx
Normal file
@@ -0,0 +1,219 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
<xsd:attribute name="type" type="xsd:string" use="optional" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" use="optional" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
|
||||
<resheader name="version"><value>2.0</value></resheader>
|
||||
<resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms</value></resheader>
|
||||
<resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms</value></resheader>
|
||||
<data name="Splash_Title" xml:space="preserve"><value>蘭山デスクトップ</value></data>
|
||||
<data name="Splash_AppName" xml:space="preserve"><value>蘭山デスクトップ</value></data>
|
||||
<data name="Splash_StatusInitializing" xml:space="preserve"><value>初期化中...</value></data>
|
||||
<data name="Splash_DebugPreview" xml:space="preserve"><value>[デバッグモード] スプラッシュプレビュー</value></data>
|
||||
<data name="Error_Title" xml:space="preserve"><value>蘭山デスクトップ</value></data>
|
||||
<data name="Error_TitleCannotConfirm" xml:space="preserve"><value>ランチャーは起動を確認できませんでした</value></data>
|
||||
<data name="Error_MessageNotReached" xml:space="preserve"><value>蘭山デスクトップが予期された起動状態に達しませんでした。</value></data>
|
||||
<data name="Error_SuggestionTitle" xml:space="preserve"><value>起動の回復</value></data>
|
||||
<data name="Error_SuggestionMessage" xml:space="preserve"><value>ログを確認するか、現在のプロセスを待機するか、実行中のデスクトップインスタンスをアクティブ化できます。</value></data>
|
||||
<data name="Error_DiagnosticHeader" xml:space="preserve"><value>診断の詳細</value></data>
|
||||
<data name="Error_ButtonOpenLogs" xml:space="preserve"><value>ログを開く</value></data>
|
||||
<data name="Error_ButtonCopy" xml:space="preserve"><value>コピー</value></data>
|
||||
<data name="Error_ButtonWait" xml:space="preserve"><value>待機</value></data>
|
||||
<data name="Error_ButtonExit" xml:space="preserve"><value>終了</value></data>
|
||||
<data name="Error_ButtonRetry" xml:space="preserve"><value>再試行</value></data>
|
||||
<data name="Error_ButtonActivate" xml:space="preserve"><value>アクティブ化</value></data>
|
||||
<data name="Error_DebugTitle" xml:space="preserve"><value>[デバッグ] ランチャーエラー</value></data>
|
||||
<data name="Error_HostNotFoundTitle" xml:space="preserve"><value>ランチャーはデスクトップ実行可能ファイルを見つけられませんでした</value></data>
|
||||
<data name="Error_HostNotFoundMessage" xml:space="preserve"><value>デバッグモードで別の実行可能ファイルを選択するか、ログを確認するか、デプロイパスを修正して再試行してください。</value></data>
|
||||
<data name="Error_GenericRetryMessage" xml:space="preserve"><value>ログを確認し、前回の起動試行が完全に終了してから再試行してください。</value></data>
|
||||
<data name="Error_GenericNoRetryMessage" xml:space="preserve"><value>ログを確認するか終了してください。古いプロセスがまだ実行中の場合、ランチャーは新しいデスクトッププロセスを作成しません。</value></data>
|
||||
<data name="Error_PendingTitle" xml:space="preserve"><value>起動はまだ保留中です</value></data>
|
||||
<data name="Error_PendingMessage" xml:space="preserve"><value>デスクトッププロセスがまだ実行中のため、ランチャーは2番目のインスタンスを起動しません。</value></data>
|
||||
<data name="Error_PendingMessageWithPid" xml:space="preserve"><value>デスクトッププロセスがまだ実行中のため、ランチャーは2番目のインスタンスを起動しません。現在のホスト PID: {0}。</value></data>
|
||||
<data name="MultiInstance_Title" xml:space="preserve"><value>蘭山デスクトップ</value></data>
|
||||
<data name="MultiInstance_AlreadyRunning" xml:space="preserve"><value>蘭山デスクトップは既に実行中です</value></data>
|
||||
<data name="MultiInstance_AlreadyRunningMessage" xml:space="preserve"><value>ランチャーは既存のデスクトップインスタンスを検出し、新しいプロセスを起動しませんでした。</value></data>
|
||||
<data name="MultiInstance_RepeatedLaunchTitle" xml:space="preserve"><value>重複起動</value></data>
|
||||
<data name="MultiInstance_RepeatedLaunchMessage" xml:space="preserve"><value>現在の設定では、デスクトップを自動的に開かずにこのプロンプトを表示します。</value></data>
|
||||
<data name="MultiInstance_NoSecondProcess" xml:space="preserve"><value>2番目のホストプロセスは作成されませんでした。</value></data>
|
||||
<data name="MultiInstance_ButtonCopy" xml:space="preserve"><value>コピー</value></data>
|
||||
<data name="MultiInstance_ButtonClose" xml:space="preserve"><value>閉じる</value></data>
|
||||
<data name="MultiInstance_ButtonOpenDesktop" xml:space="preserve"><value>デスクトップを開く</value></data>
|
||||
<data name="MultiInstance_DetailsFormat" xml:space="preserve"><value>既存のホスト PID: {0} シェル状態: {1} 2番目のホストプロセスは作成されませんでした。</value></data>
|
||||
<data name="DataLocation_Title" xml:space="preserve"><value>データ保存場所の選択</value></data>
|
||||
<data name="DataLocation_ChooseLocation" xml:space="preserve"><value>データ保存場所の選択</value></data>
|
||||
<data name="DataLocation_ChooseLocationDesc" xml:space="preserve"><value>ランチャーとデスクトップデータの保存場所を選択してください。後から設定で変更できます。</value></data>
|
||||
<data name="DataLocation_NotWritable" xml:space="preserve"><value>アプリフォルダに書き込みできません</value></data>
|
||||
<data name="DataLocation_NotWritableDesc" xml:space="preserve"><value>現在のインストールディレクトリには昇格された権限が必要です。データはシステムユーザープロファイルに保存されます。</value></data>
|
||||
<data name="DataLocation_SystemProfile" xml:space="preserve"><value>システムユーザープロファイルに保存(推奨)</value></data>
|
||||
<data name="DataLocation_SystemProfileDesc" xml:space="preserve"><value>データは現在のWindowsユーザーに紐付けられ、アプリの再インストールや更新後も保持されます。</value></data>
|
||||
<data name="DataLocation_Portable" xml:space="preserve"><value>アプリの横に保存(ポータブル)</value></data>
|
||||
<data name="DataLocation_PortableDesc" xml:space="preserve"><value>ポータブルインストールに便利です。アプリフォルダ全体をデータと一緒に別のマシンに移動できます。</value></data>
|
||||
<data name="DataLocation_ButtonCancel" xml:space="preserve"><value>キャンセル</value></data>
|
||||
<data name="DataLocation_ButtonConfirm" xml:space="preserve"><value>確認</value></data>
|
||||
<data name="DataLocation_MigrateWarning" xml:space="preserve"><value>既存のシステムデータが検出されました。ポータブルモードを選択すると、現在のデータが自動的に移行されます。</value></data>
|
||||
<data name="Loading_Title" xml:space="preserve"><value>蘭山デスクトップ - 読み込み詳細</value></data>
|
||||
<data name="Loading_StartingDesktop" xml:space="preserve"><value>蘭山デスクトップを起動中</value></data>
|
||||
<data name="Loading_StatusInitializing" xml:space="preserve"><value>初期化中...</value></data>
|
||||
<data name="Loading_StatusPreparing" xml:space="preserve"><value>コンポーネントを準備中</value></data>
|
||||
<data name="Loading_LoadingItems" xml:space="preserve"><value>読み込み項目</value></data>
|
||||
<data name="Loading_Done" xml:space="preserve"><value>完了</value></data>
|
||||
<data name="Loading_ErrorOccurred" xml:space="preserve"><value>読み込み中にエラーが発生しました。</value></data>
|
||||
<data name="Loading_ButtonDetails" xml:space="preserve"><value>詳細</value></data>
|
||||
<data name="Loading_ButtonCancel" xml:space="preserve"><value>キャンセル</value></data>
|
||||
<data name="Loading_StageReady" xml:space="preserve"><value>準備完了</value></data>
|
||||
<data name="Loading_ItemPlugin" xml:space="preserve"><value>プラグインを読み込み中...</value></data>
|
||||
<data name="Loading_ItemComponent" xml:space="preserve"><value>コンポーネントを読み込み中...</value></data>
|
||||
<data name="Loading_ItemResource" xml:space="preserve"><value>リソースを読み込み中...</value></data>
|
||||
<data name="Loading_ItemData" xml:space="preserve"><value>データを読み込み中...</value></data>
|
||||
<data name="Loading_ItemDownload" xml:space="preserve"><value>ダウンロード中...</value></data>
|
||||
<data name="Loading_ItemProcess" xml:space="preserve"><value>処理中...</value></data>
|
||||
<data name="Loading_ItemComplete" xml:space="preserve"><value>完了</value></data>
|
||||
<data name="Loading_TypePlugin" xml:space="preserve"><value>プラグイン</value></data>
|
||||
<data name="Loading_TypeComponent" xml:space="preserve"><value>コンポーネント</value></data>
|
||||
<data name="Loading_TypeResource" xml:space="preserve"><value>リソース</value></data>
|
||||
<data name="Loading_TypeData" xml:space="preserve"><value>データ</value></data>
|
||||
<data name="Loading_TypeNetwork" xml:space="preserve"><value>ネットワーク</value></data>
|
||||
<data name="Loading_TypeSettings" xml:space="preserve"><value>設定</value></data>
|
||||
<data name="Loading_TypeSystem" xml:space="preserve"><value>システム</value></data>
|
||||
<data name="Loading_TypeOther" xml:space="preserve"><value>その他</value></data>
|
||||
<data name="Update_Title" xml:space="preserve"><value>蘭山デスクトップ - 更新</value></data>
|
||||
<data name="Update_AppName" xml:space="preserve"><value>蘭山デスクトップ</value></data>
|
||||
<data name="Update_StatusUpdate" xml:space="preserve"><value>更新</value></data>
|
||||
<data name="Update_StatusUpdating" xml:space="preserve"><value>更新中、お待ちください...</value></data>
|
||||
<data name="Update_Complete" xml:space="preserve"><value>更新完了</value></data>
|
||||
<data name="Update_Failed" xml:space="preserve"><value>更新失敗</value></data>
|
||||
<data name="Update_FailedMessage" xml:space="preserve"><value>更新中にエラーが発生しました</value></data>
|
||||
<data name="Update_DebugTitle" xml:space="preserve"><value>[デバッグモード] 更新ページ</value></data>
|
||||
<data name="Update_DebugMessage" xml:space="preserve"><value>更新進行インターフェースのプレビュー</value></data>
|
||||
<data name="Update_Verifying" xml:space="preserve"><value>更新を検証中...</value></data>
|
||||
<data name="Update_ApplyingPlugins" xml:space="preserve"><value>プラグインのアップグレードを適用中...</value></data>
|
||||
<data name="Update_CleaningUp" xml:space="preserve"><value>古いデプロイメントをクリーンアップ中...</value></data>
|
||||
<data name="DebugDebug_Title" xml:space="preserve"><value>デバッグモード</value></data>
|
||||
<data name="DebugDebug_SettingsTitle" xml:space="preserve"><value>デバッグ設定</value></data>
|
||||
<data name="DebugDebug_DevMode" xml:space="preserve"><value>開発者モード</value></data>
|
||||
<data name="DebugDebug_DevModeDesc" xml:space="preserve"><value>有効にすると開発ディレクトリを自動スキャンします</value></data>
|
||||
<data name="DebugDebug_On" xml:space="preserve"><value>オン</value></data>
|
||||
<data name="DebugDebug_Off" xml:space="preserve"><value>オフ</value></data>
|
||||
<data name="DebugDebug_AppPath" xml:space="preserve"><value>アプリパス</value></data>
|
||||
<data name="DebugDebug_NotSelected" xml:space="preserve"><value>未選択</value></data>
|
||||
<data name="DebugDebug_Browse" xml:space="preserve"><value>参照...</value></data>
|
||||
<data name="DebugDebug_Warning" xml:space="preserve"><value>この機能は開発者専用です</value></data>
|
||||
<data name="DebugDebug_ButtonCancel" xml:space="preserve"><value>キャンセル</value></data>
|
||||
<data name="DebugDebug_ButtonOk" xml:space="preserve"><value>OK</value></data>
|
||||
<data name="DebugDebug_SelectExeDialog" xml:space="preserve"><value>蘭山デスクトップホスト実行可能ファイルを選択</value></data>
|
||||
<data name="Oobe_Title" xml:space="preserve"><value>蘭山デスクトップへようこそ</value></data>
|
||||
<data name="Oobe_WelcomeTitle" xml:space="preserve"><value>蘭山デスクトップへようこそ</value></data>
|
||||
<data name="Oobe_WelcomeSubtitle" xml:space="preserve"><value>あなたのデスクトップ、一面だけじゃない</value></data>
|
||||
<data name="Oobe_ButtonGetStarted" xml:space="preserve"><value>始める</value></data>
|
||||
<data name="Oobe_AppearanceTitle" xml:space="preserve"><value>デスクトップをカスタマイズ</value></data>
|
||||
<data name="Oobe_AppearanceDesc" xml:space="preserve"><value>お好みのテーマスタイルを選択してください。設定でいつでも変更できます。</value></data>
|
||||
<data name="Oobe_AppearanceMode" xml:space="preserve"><value>外観モード</value></data>
|
||||
<data name="Oobe_LightMode" xml:space="preserve"><value>ライトモード</value></data>
|
||||
<data name="Oobe_DarkMode" xml:space="preserve"><value>ダークモード</value></data>
|
||||
<data name="Oobe_ThemeColor" xml:space="preserve"><value>テーマカラー</value></data>
|
||||
<data name="Oobe_MonetSource" xml:space="preserve"><value>Monet カラーソース</value></data>
|
||||
<data name="Oobe_MonetFromWallpaper" xml:space="preserve"><value>壁紙から抽出</value></data>
|
||||
<data name="Oobe_MonetFromCustomImage" xml:space="preserve"><value>カスタム画像から抽出</value></data>
|
||||
<data name="Oobe_MonetDisabled" xml:space="preserve"><value>Monet カラーを使用しない</value></data>
|
||||
<data name="Oobe_DataLocationTitle" xml:space="preserve"><value>データ保存場所の選択</value></data>
|
||||
<data name="Oobe_SystemProfile" xml:space="preserve"><value>システムユーザープロファイルに保存(推奨)</value></data>
|
||||
<data name="Oobe_SystemProfileDesc" xml:space="preserve"><value>データは現在のWindowsユーザーに紐付けられ、アプリの再インストールや更新後も保持されます。</value></data>
|
||||
<data name="Oobe_Portable" xml:space="preserve"><value>アプリの横に保存(ポータブル)</value></data>
|
||||
<data name="Oobe_PortableDesc" xml:space="preserve"><value>ポータブルインストールに便利です。アプリフォルダ全体をデータと一緒に別のマシンに移動できます。</value></data>
|
||||
<data name="Oobe_NotWritable" xml:space="preserve"><value>アプリディレクトリに保存できません</value></data>
|
||||
<data name="Oobe_NotWritableDesc" xml:space="preserve"><value>現在のインストールディレクトリには昇格された権限が必要です。データはシステムユーザープロファイルに保存されます。</value></data>
|
||||
<data name="Oobe_StartupTitle" xml:space="preserve"><value>起動と表示</value></data>
|
||||
<data name="Oobe_ShowInTaskbar" xml:space="preserve"><value>タスクバーにメインデスクトップウィンドウを表示</value></data>
|
||||
<data name="Oobe_SlideTransition" xml:space="preserve"><value>スライド遷移でメインウィンドウを表示</value></data>
|
||||
<data name="Oobe_FadeTransition" xml:space="preserve"><value>起動時にフェードイン遷移を使用</value></data>
|
||||
<data name="Oobe_FusedDesktop" xml:space="preserve"><value>フューズドデスクトップとスワイプジェスチャー</value></data>
|
||||
<data name="Oobe_AutoStart" xml:space="preserve"><value>Windowsログイン時に蘭山デスクトップを自動起動</value></data>
|
||||
<data name="Oobe_PrivacyTitle" xml:space="preserve"><value>情報とプライバシー</value></data>
|
||||
<data name="Oobe_CrashReports" xml:space="preserve"><value>匿名クラッシュレポートを送信</value></data>
|
||||
<data name="Oobe_UsageStats" xml:space="preserve"><value>匿名使用統計を送信</value></data>
|
||||
<data name="Oobe_PrivacyTrackingId" xml:space="preserve"><value>プライバシー追跡 ID</value></data>
|
||||
<data name="Oobe_Agree" xml:space="preserve"><value>同意する</value></data>
|
||||
<data name="Oobe_PrivacyPolicyLink" xml:space="preserve"><value>蘭山デスクトップテレメトリプライバシーデータ収集同意書</value></data>
|
||||
<data name="Oobe_ButtonBack" xml:space="preserve"><value>戻る</value></data>
|
||||
<data name="Oobe_ButtonNext" xml:space="preserve"><value>次へ</value></data>
|
||||
<data name="Oobe_CompleteTitle" xml:space="preserve"><value>蘭山デスクトップへようこそ</value></data>
|
||||
<data name="Oobe_CompleteSubtitle" xml:space="preserve"><value>あなたのデスクトップ、一面だけじゃない</value></data>
|
||||
<data name="Oobe_MonetDesc" xml:space="preserve"><value>壁紙からテーマカラーを自動抽出し、デスクトップとシームレスに融合</value></data>
|
||||
<data name="Oobe_MonetFromWallpaperDesc" xml:space="preserve"><value>現在の壁紙の色を分析してテーマを生成</value></data>
|
||||
<data name="Oobe_MonetFromCustomImageDesc" xml:space="preserve"><value>画像を選択してカラーソースにする</value></data>
|
||||
<data name="Oobe_MonetDisabledDesc" xml:space="preserve"><value>固定のプリセットテーマカラーを使用</value></data>
|
||||
<data name="Oobe_DataLocationDesc" xml:space="preserve"><value>アプリデータの保存場所を決定します。設定でいつでも変更できます。</value></data>
|
||||
<data name="Oobe_StartupDesc" xml:space="preserve"><value>これらのオプションはデスクトップアプリの設定でいつでも変更できます。スライドインはWindowsでのみ利用可能です。</value></data>
|
||||
<data name="Oobe_ShowInTaskbarDesc" xml:space="preserve"><value>有効にすると最小化時にタスクバーにエントリが残ります。無効にするとトレイアイコンに依存します。</value></data>
|
||||
<data name="Oobe_SlideTransitionDesc" xml:space="preserve"><value>画面端からスライドイン。フェードインとは排他的。</value></data>
|
||||
<data name="Oobe_FadeTransitionDesc" xml:space="preserve"><value>スライドインが無効の場合に推奨。</value></data>
|
||||
<data name="Oobe_FusedDesktopDesc" xml:space="preserve"><value>フューズドデスクトップと三本指スワイプジェスチャーを有効にし、エッジポップインと関連実験機能を使用します(設定の開発者オプションと同じ)。</value></data>
|
||||
<data name="Oobe_AutoStartDesc" xml:space="preserve"><value>このランチャーを現在のユーザーのスタートアップ項目として登録します(インストーラーのオプションタスクと同じレジストリエントリ)。</value></data>
|
||||
<data name="Oobe_AutoStartDescNonWindows" xml:space="preserve"><value>このプラットフォームでは設定のみ保存されます。自動起動にはシステムのアプリ自動起動設定を使用してください。</value></data>
|
||||
<data name="Oobe_PrivacyDesc" xml:space="preserve"><value>テレメトリプログラムへの参加を選択し、プライバシーポリシーを確認</value></data>
|
||||
<data name="Oobe_CrashReportsDesc" xml:space="preserve"><value>アプリの安定性向上に協力。個人情報は含まれません</value></data>
|
||||
<data name="Oobe_UsageStatsDesc" xml:space="preserve"><value>機能の使用状況を把握し、製品体験を最適化</value></data>
|
||||
<data name="Oobe_PrivacyTrackingIdDesc" xml:space="preserve"><value>このIDはデバイスを匿名で識別するために使用され、個人情報は含まれません</value></data>
|
||||
<data name="Oobe_PrivacyAgreementNote" xml:space="preserve"><value>テレメトリ機能を有効にする前に、プライバシーポリシーを読んで同意する必要があります。テレメトリデータはアプリの安定性向上と製品体験の最適化にのみ使用され、個人情報は含まれません。</value></data>
|
||||
<data name="Oobe_TypingAppName" xml:space="preserve"><value>蘭山デスクトップ LanMountain Desktop</value></data>
|
||||
<data name="Oobe_TypingNextGen" xml:space="preserve"><value>次世代</value></data>
|
||||
<data name="Oobe_TypingDashboard" xml:space="preserve"><value>インタラクティブダッシュボード</value></data>
|
||||
<data name="Oobe_MigrationDetected" xml:space="preserve"><value>既存データが検出されました。ポータブルモード選択時に自動移行されます。</value></data>
|
||||
<data name="Migration_Title" xml:space="preserve"><value>蘭山デスクトップ - バージョン移行</value></data>
|
||||
<data name="Migration_DetectedOldVersion" xml:space="preserve"><value>旧バージョンを検出</value></data>
|
||||
<data name="Migration_DetectedDesc" xml:space="preserve"><value>システムに旧バージョンの蘭山デスクトップ(0.8.4)が検出されました。競合を避けるためにアンインストールをお勧めします。</value></data>
|
||||
<data name="Migration_Version" xml:space="preserve"><value>バージョン:</value></data>
|
||||
<data name="Migration_Location" xml:space="preserve"><value>場所:</value></data>
|
||||
<data name="Migration_Type" xml:space="preserve"><value>タイプ:</value></data>
|
||||
<data name="Migration_Installed" xml:space="preserve"><value>インストール版</value></data>
|
||||
<data name="Migration_UninstallNote" xml:space="preserve"><value>旧バージョンをアンインストールしても新バージョンには影響しません。個人データは保持されます。</value></data>
|
||||
<data name="Migration_ButtonViewLocation" xml:space="preserve"><value>場所を表示</value></data>
|
||||
<data name="Migration_ButtonSkip" xml:space="preserve"><value>後で</value></data>
|
||||
<data name="Migration_ButtonUninstall" xml:space="preserve"><value>旧バージョンをアンインストール</value></data>
|
||||
<data name="Migration_Portable" xml:space="preserve"><value>ポータブル版</value></data>
|
||||
<data name="Migration_Unknown" xml:space="preserve"><value>不明</value></data>
|
||||
<data name="Migration_DetectedDescFormat" xml:space="preserve"><value>システムに旧バージョンの蘭山デスクトップ({0})が検出されました。新バージョンは完全に新しいアーキテクチャを採用しています。より良い体験のために旧バージョンのアンインストールをお勧めします。</value></data>
|
||||
<data name="Privacy_Title" xml:space="preserve"><value>蘭山デスクトップテレメトリプライバシーデータ収集同意書</value></data>
|
||||
<data name="Privacy_Header" xml:space="preserve"><value>蘭山デスクトップテレメトリプライバシーデータ収集同意書</value></data>
|
||||
<data name="Privacy_Description" xml:space="preserve"><value>以下の同意書をよくお読みになり、データの収集、使用、保護についてご理解ください</value></data>
|
||||
<data name="Privacy_ButtonClose" xml:space="preserve"><value>閉じる</value></data>
|
||||
<data name="DevDebug_Title" xml:space="preserve"><value>開発者デバッグウィンドウ</value></data>
|
||||
<data name="DevDebug_Splash" xml:space="preserve"><value>スプラッシュ画面</value></data>
|
||||
<data name="DevDebug_Error" xml:space="preserve"><value>エラーページ</value></data>
|
||||
<data name="DevDebug_Update" xml:space="preserve"><value>更新ページ</value></data>
|
||||
<data name="DevDebug_Oobe" xml:space="preserve"><value>OOBEページ</value></data>
|
||||
<data name="DevDebug_DataLocation" xml:space="preserve"><value>データ保存場所</value></data>
|
||||
<data name="DevDebug_EnableFeature" xml:space="preserve"><value>機能を有効化</value></data>
|
||||
<data name="DevDebug_Open" xml:space="preserve"><value>開く</value></data>
|
||||
<data name="DevDebug_SetAllViewMode" xml:space="preserve"><value>すべて表示モードに設定</value></data>
|
||||
<data name="DevDebug_SetAllFunctionMode" xml:space="preserve"><value>すべて機能モードに設定</value></data>
|
||||
<data name="DevDebug_Close" xml:space="preserve"><value>閉じる</value></data>
|
||||
<data name="Coordinator_SlowDeviceMessage" xml:space="preserve"><value>デバイスの動作が遅く、まだ起動中です。お待ちください。</value></data>
|
||||
<data name="Coordinator_RunningHostMessage" xml:space="preserve"><value>デスクトッププロセスがまだ実行中です。ランチャーは待機を続け、再起動しません。</value></data>
|
||||
<data name="Preview_SplashInitializing" xml:space="preserve"><value>初期化中...</value></data>
|
||||
<data name="Preview_SplashCheckingUpdates" xml:space="preserve"><value>更新を確認中...</value></data>
|
||||
<data name="Preview_SplashCheckingPlugins" xml:space="preserve"><value>プラグインを確認中...</value></data>
|
||||
<data name="Preview_SplashLaunchingHost" xml:space="preserve"><value>ホストを起動中...</value></data>
|
||||
<data name="Preview_SplashReady" xml:space="preserve"><value>準備完了</value></data>
|
||||
<data name="Preview_ErrorMessage" xml:space="preserve"><value>[プレビュー] ランチャーエラーウィンドウのプレビューです。</value></data>
|
||||
<data name="Preview_UpdateProcessing" xml:space="preserve"><value>{0} を処理中...</value></data>
|
||||
<data name="Preview_ActivationConnecting" xml:space="preserve"><value>アクティブなランチャーに接続中...</value></data>
|
||||
</root>
|
||||
219
LanMountainDesktop.Launcher/Resources/Strings.ko-KR.resx
Normal file
219
LanMountainDesktop.Launcher/Resources/Strings.ko-KR.resx
Normal file
@@ -0,0 +1,219 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
<xsd:attribute name="type" type="xsd:string" use="optional" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" use="optional" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
|
||||
<resheader name="version"><value>2.0</value></resheader>
|
||||
<resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms</value></resheader>
|
||||
<resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms</value></resheader>
|
||||
<data name="Splash_Title" xml:space="preserve"><value>란산 데스크톱</value></data>
|
||||
<data name="Splash_AppName" xml:space="preserve"><value>란산 데스크톱</value></data>
|
||||
<data name="Splash_StatusInitializing" xml:space="preserve"><value>초기화 중...</value></data>
|
||||
<data name="Splash_DebugPreview" xml:space="preserve"><value>[디버그 모드] 스플래시 미리보기</value></data>
|
||||
<data name="Error_Title" xml:space="preserve"><value>란산 데스크톱</value></data>
|
||||
<data name="Error_TitleCannotConfirm" xml:space="preserve"><value>런처가 시작을 확인할 수 없습니다</value></data>
|
||||
<data name="Error_MessageNotReached" xml:space="preserve"><value>란산 데스크톱이 예상된 시작 상태에 도달하지 못했습니다.</value></data>
|
||||
<data name="Error_SuggestionTitle" xml:space="preserve"><value>시작 복구</value></data>
|
||||
<data name="Error_SuggestionMessage" xml:space="preserve"><value>로그를 검사하거나, 현재 프로세스를 대기하거나, 실행 중인 데스크톱 인스턴스를 활성화할 수 있습니다.</value></data>
|
||||
<data name="Error_DiagnosticHeader" xml:space="preserve"><value>진단 세부정보</value></data>
|
||||
<data name="Error_ButtonOpenLogs" xml:space="preserve"><value>로그 열기</value></data>
|
||||
<data name="Error_ButtonCopy" xml:space="preserve"><value>복사</value></data>
|
||||
<data name="Error_ButtonWait" xml:space="preserve"><value>대기</value></data>
|
||||
<data name="Error_ButtonExit" xml:space="preserve"><value>종료</value></data>
|
||||
<data name="Error_ButtonRetry" xml:space="preserve"><value>재시도</value></data>
|
||||
<data name="Error_ButtonActivate" xml:space="preserve"><value>활성화</value></data>
|
||||
<data name="Error_DebugTitle" xml:space="preserve"><value>[디버그] 런처 오류</value></data>
|
||||
<data name="Error_HostNotFoundTitle" xml:space="preserve"><value>런처가 데스크톱 실행 파일을 찾을 수 없습니다</value></data>
|
||||
<data name="Error_HostNotFoundMessage" xml:space="preserve"><value>디버그 모드에서 다른 실행 파일을 선택하거나, 로그를 검사하거나, 배포 경로를 수정한 후 재시도하세요.</value></data>
|
||||
<data name="Error_GenericRetryMessage" xml:space="preserve"><value>로그를 검사한 후 이전 시작 시도가 완전히 끝나면 재시도하세요.</value></data>
|
||||
<data name="Error_GenericNoRetryMessage" xml:space="preserve"><value>로그를 검사하거나 종료하세요. 이전 프로세스가 여전히 실행 중인 동안 런처는 새 데스크톱 프로세스를 생성하지 않습니다.</value></data>
|
||||
<data name="Error_PendingTitle" xml:space="preserve"><value>시작이 아직 보류 중입니다</value></data>
|
||||
<data name="Error_PendingMessage" xml:space="preserve"><value>데스크톱 프로세스가 여전히 실행 중이므로 런처는 두 번째 인스턴스를 시작하지 않습니다.</value></data>
|
||||
<data name="Error_PendingMessageWithPid" xml:space="preserve"><value>데스크톱 프로세스가 여전히 실행 중이므로 런처는 두 번째 인스턴스를 시작하지 않습니다. 현재 호스트 PID: {0}.</value></data>
|
||||
<data name="MultiInstance_Title" xml:space="preserve"><value>란산 데스크톱</value></data>
|
||||
<data name="MultiInstance_AlreadyRunning" xml:space="preserve"><value>란산 데스크톱이 이미 실행 중입니다</value></data>
|
||||
<data name="MultiInstance_AlreadyRunningMessage" xml:space="preserve"><value>런처가 기존 데스크톱 인스턴스를 감지하여 새 프로세스를 시작하지 않았습니다.</value></data>
|
||||
<data name="MultiInstance_RepeatedLaunchTitle" xml:space="preserve"><value>중복 실행</value></data>
|
||||
<data name="MultiInstance_RepeatedLaunchMessage" xml:space="preserve"><value>현재 설정은 데스크톱을 자동으로 열지 않고 이 프롬프트를 표시하는 것입니다.</value></data>
|
||||
<data name="MultiInstance_NoSecondProcess" xml:space="preserve"><value>두 번째 호스트 프로세스가 생성되지 않았습니다.</value></data>
|
||||
<data name="MultiInstance_ButtonCopy" xml:space="preserve"><value>복사</value></data>
|
||||
<data name="MultiInstance_ButtonClose" xml:space="preserve"><value>닫기</value></data>
|
||||
<data name="MultiInstance_ButtonOpenDesktop" xml:space="preserve"><value>데스크톱 열기</value></data>
|
||||
<data name="MultiInstance_DetailsFormat" xml:space="preserve"><value>기존 호스트 PID: {0} 셸 상태: {1} 두 번째 호스트 프로세스가 생성되지 않았습니다.</value></data>
|
||||
<data name="DataLocation_Title" xml:space="preserve"><value>데이터 저장 위치 선택</value></data>
|
||||
<data name="DataLocation_ChooseLocation" xml:space="preserve"><value>데이터 저장 위치 선택</value></data>
|
||||
<data name="DataLocation_ChooseLocationDesc" xml:space="preserve"><value>런처와 데스크톱 데이터가 저장될 위치를 선택하세요. 나중에 설정에서 변경할 수 있습니다.</value></data>
|
||||
<data name="DataLocation_NotWritable" xml:space="preserve"><value>앱 폴더에 쓸 수 없습니다</value></data>
|
||||
<data name="DataLocation_NotWritableDesc" xml:space="preserve"><value>현재 설치 디렉토리는 상승된 권한이 필요합니다. 데이터는 시스템 사용자 프로필에 저장됩니다.</value></data>
|
||||
<data name="DataLocation_SystemProfile" xml:space="preserve"><value>시스템 사용자 프로필에 저장 (권장)</value></data>
|
||||
<data name="DataLocation_SystemProfileDesc" xml:space="preserve"><value>데이터는 현재 Windows 사용자에 연결되며, 앱 재설치 및 업데이트 후에도 그대로 유지됩니다.</value></data>
|
||||
<data name="DataLocation_Portable" xml:space="preserve"><value>앱 옆에 저장 (휴대용)</value></data>
|
||||
<data name="DataLocation_PortableDesc" xml:space="preserve"><value>휴대용 설치에 유용합니다. 전체 앱 폴더를 데이터와 함께 다른 컴퓨터로 이동할 수 있습니다.</value></data>
|
||||
<data name="DataLocation_ButtonCancel" xml:space="preserve"><value>취소</value></data>
|
||||
<data name="DataLocation_ButtonConfirm" xml:space="preserve"><value>확인</value></data>
|
||||
<data name="DataLocation_MigrateWarning" xml:space="preserve"><value>기존 시스템 데이터가 감지되었습니다. 휴대용 모드를 선택하면 현재 데이터가 자동으로 마이그레이션됩니다.</value></data>
|
||||
<data name="Loading_Title" xml:space="preserve"><value>란산 데스크톱 - 로딩 세부정보</value></data>
|
||||
<data name="Loading_StartingDesktop" xml:space="preserve"><value>란산 데스크톱 시작 중</value></data>
|
||||
<data name="Loading_StatusInitializing" xml:space="preserve"><value>초기화 중...</value></data>
|
||||
<data name="Loading_StatusPreparing" xml:space="preserve"><value>구성 요소 준비 중</value></data>
|
||||
<data name="Loading_LoadingItems" xml:space="preserve"><value>로딩 항목</value></data>
|
||||
<data name="Loading_Done" xml:space="preserve"><value>완료</value></data>
|
||||
<data name="Loading_ErrorOccurred" xml:space="preserve"><value>로딩 중 오류가 발생했습니다.</value></data>
|
||||
<data name="Loading_ButtonDetails" xml:space="preserve"><value>세부정보</value></data>
|
||||
<data name="Loading_ButtonCancel" xml:space="preserve"><value>취소</value></data>
|
||||
<data name="Loading_StageReady" xml:space="preserve"><value>준비 완료</value></data>
|
||||
<data name="Loading_ItemPlugin" xml:space="preserve"><value>플러그인 로딩 중...</value></data>
|
||||
<data name="Loading_ItemComponent" xml:space="preserve"><value>구성 요소 로딩 중...</value></data>
|
||||
<data name="Loading_ItemResource" xml:space="preserve"><value>리소스 로딩 중...</value></data>
|
||||
<data name="Loading_ItemData" xml:space="preserve"><value>데이터 로딩 중...</value></data>
|
||||
<data name="Loading_ItemDownload" xml:space="preserve"><value>다운로드 중...</value></data>
|
||||
<data name="Loading_ItemProcess" xml:space="preserve"><value>처리 중...</value></data>
|
||||
<data name="Loading_ItemComplete" xml:space="preserve"><value>완료</value></data>
|
||||
<data name="Loading_TypePlugin" xml:space="preserve"><value>플러그인</value></data>
|
||||
<data name="Loading_TypeComponent" xml:space="preserve"><value>구성 요소</value></data>
|
||||
<data name="Loading_TypeResource" xml:space="preserve"><value>리소스</value></data>
|
||||
<data name="Loading_TypeData" xml:space="preserve"><value>데이터</value></data>
|
||||
<data name="Loading_TypeNetwork" xml:space="preserve"><value>네트워크</value></data>
|
||||
<data name="Loading_TypeSettings" xml:space="preserve"><value>설정</value></data>
|
||||
<data name="Loading_TypeSystem" xml:space="preserve"><value>시스템</value></data>
|
||||
<data name="Loading_TypeOther" xml:space="preserve"><value>기타</value></data>
|
||||
<data name="Update_Title" xml:space="preserve"><value>란산 데스크톱 - 업데이트</value></data>
|
||||
<data name="Update_AppName" xml:space="preserve"><value>란산 데스크톱</value></data>
|
||||
<data name="Update_StatusUpdate" xml:space="preserve"><value>업데이트</value></data>
|
||||
<data name="Update_StatusUpdating" xml:space="preserve"><value>업데이트 중, 잠시 기다려 주세요...</value></data>
|
||||
<data name="Update_Complete" xml:space="preserve"><value>업데이트 완료</value></data>
|
||||
<data name="Update_Failed" xml:space="preserve"><value>업데이트 실패</value></data>
|
||||
<data name="Update_FailedMessage" xml:space="preserve"><value>업데이트 중 오류가 발생했습니다</value></data>
|
||||
<data name="Update_DebugTitle" xml:space="preserve"><value>[디버그 모드] 업데이트 페이지</value></data>
|
||||
<data name="Update_DebugMessage" xml:space="preserve"><value>업데이트 진행 인터페이스 미리보기</value></data>
|
||||
<data name="Update_Verifying" xml:space="preserve"><value>업데이트 확인 중...</value></data>
|
||||
<data name="Update_ApplyingPlugins" xml:space="preserve"><value>플러그인 업그레이드 적용 중...</value></data>
|
||||
<data name="Update_CleaningUp" xml:space="preserve"><value>이전 배포 정리 중...</value></data>
|
||||
<data name="DebugDebug_Title" xml:space="preserve"><value>디버그 모드</value></data>
|
||||
<data name="DebugDebug_SettingsTitle" xml:space="preserve"><value>디버그 설정</value></data>
|
||||
<data name="DebugDebug_DevMode" xml:space="preserve"><value>개발자 모드</value></data>
|
||||
<data name="DebugDebug_DevModeDesc" xml:space="preserve"><value>활성화 시 개발 디렉토리 자동 스캔</value></data>
|
||||
<data name="DebugDebug_On" xml:space="preserve"><value>켜기</value></data>
|
||||
<data name="DebugDebug_Off" xml:space="preserve"><value>끄기</value></data>
|
||||
<data name="DebugDebug_AppPath" xml:space="preserve"><value>앱 경로</value></data>
|
||||
<data name="DebugDebug_NotSelected" xml:space="preserve"><value>선택 안 됨</value></data>
|
||||
<data name="DebugDebug_Browse" xml:space="preserve"><value>찾아보기...</value></data>
|
||||
<data name="DebugDebug_Warning" xml:space="preserve"><value>이 기능은 개발자 전용입니다</value></data>
|
||||
<data name="DebugDebug_ButtonCancel" xml:space="preserve"><value>취소</value></data>
|
||||
<data name="DebugDebug_ButtonOk" xml:space="preserve"><value>확인</value></data>
|
||||
<data name="DebugDebug_SelectExeDialog" xml:space="preserve"><value>란산 데스크톱 호스트 실행 파일 선택</value></data>
|
||||
<data name="Oobe_Title" xml:space="preserve"><value>란산 데스크톱에 오신 것을 환영합니다</value></data>
|
||||
<data name="Oobe_WelcomeTitle" xml:space="preserve"><value>란산 데스크톱에 오신 것을 환영합니다</value></data>
|
||||
<data name="Oobe_WelcomeSubtitle" xml:space="preserve"><value>당신의 데스크톱, 한 면이 아닙니다</value></data>
|
||||
<data name="Oobe_ButtonGetStarted" xml:space="preserve"><value>시작하기</value></data>
|
||||
<data name="Oobe_AppearanceTitle" xml:space="preserve"><value>데스크톱 개인화</value></data>
|
||||
<data name="Oobe_AppearanceDesc" xml:space="preserve"><value>원하는 테마 스타일을 선택하세요. 설정에서 언제든 변경할 수 있습니다.</value></data>
|
||||
<data name="Oobe_AppearanceMode" xml:space="preserve"><value>외관 모드</value></data>
|
||||
<data name="Oobe_LightMode" xml:space="preserve"><value>라이트 모드</value></data>
|
||||
<data name="Oobe_DarkMode" xml:space="preserve"><value>다크 모드</value></data>
|
||||
<data name="Oobe_ThemeColor" xml:space="preserve"><value>테마 색상</value></data>
|
||||
<data name="Oobe_MonetSource" xml:space="preserve"><value>Monet 색상 소스</value></data>
|
||||
<data name="Oobe_MonetFromWallpaper" xml:space="preserve"><value>바탕 화면에서 추출</value></data>
|
||||
<data name="Oobe_MonetFromCustomImage" xml:space="preserve"><value>사용자 지정 이미지에서 추출</value></data>
|
||||
<data name="Oobe_MonetDisabled" xml:space="preserve"><value>Monet 색상 사용 안 함</value></data>
|
||||
<data name="Oobe_DataLocationTitle" xml:space="preserve"><value>데이터 저장 위치 선택</value></data>
|
||||
<data name="Oobe_SystemProfile" xml:space="preserve"><value>시스템 사용자 프로필에 저장 (권장)</value></data>
|
||||
<data name="Oobe_SystemProfileDesc" xml:space="preserve"><value>데이터는 현재 Windows 사용자에 연결되며, 앱 재설치 및 업데이트 후에도 그대로 유지됩니다.</value></data>
|
||||
<data name="Oobe_Portable" xml:space="preserve"><value>앱 옆에 저장 (휴대용)</value></data>
|
||||
<data name="Oobe_PortableDesc" xml:space="preserve"><value>휴대용 설치에 유용합니다. 전체 앱 폴더를 데이터와 함께 다른 컴퓨터로 이동할 수 있습니다.</value></data>
|
||||
<data name="Oobe_NotWritable" xml:space="preserve"><value>앱 디렉토리에 저장할 수 없습니다</value></data>
|
||||
<data name="Oobe_NotWritableDesc" xml:space="preserve"><value>현재 설치 디렉토리는 상승된 권한이 필요합니다. 데이터는 시스템 사용자 프로필에 저장됩니다.</value></data>
|
||||
<data name="Oobe_StartupTitle" xml:space="preserve"><value>시작 및 표시</value></data>
|
||||
<data name="Oobe_ShowInTaskbar" xml:space="preserve"><value>작업 표시줄에 기본 데스크톱 창 표시</value></data>
|
||||
<data name="Oobe_SlideTransition" xml:space="preserve"><value>슬라이드 전환으로 기본 창 표시</value></data>
|
||||
<data name="Oobe_FadeTransition" xml:space="preserve"><value>시작 시 페이드인 전환 사용</value></data>
|
||||
<data name="Oobe_FusedDesktop" xml:space="preserve"><value>퓨즈드 데스크톱 및 스와이프 제스처</value></data>
|
||||
<data name="Oobe_AutoStart" xml:space="preserve"><value>Windows 로그인 시 란산 데스크톱 자동 시작</value></data>
|
||||
<data name="Oobe_PrivacyTitle" xml:space="preserve"><value>정보 및 개인정보</value></data>
|
||||
<data name="Oobe_CrashReports" xml:space="preserve"><value>익명 크래시 보고서 보내기</value></data>
|
||||
<data name="Oobe_UsageStats" xml:space="preserve"><value>익명 사용 통계 보내기</value></data>
|
||||
<data name="Oobe_PrivacyTrackingId" xml:space="preserve"><value>개인정보 추적 ID</value></data>
|
||||
<data name="Oobe_Agree" xml:space="preserve"><value>동의</value></data>
|
||||
<data name="Oobe_PrivacyPolicyLink" xml:space="preserve"><value>란산 데스크톱 원격 측정 개인정보 데이터 수집 동의서</value></data>
|
||||
<data name="Oobe_ButtonBack" xml:space="preserve"><value>뒤로</value></data>
|
||||
<data name="Oobe_ButtonNext" xml:space="preserve"><value>다음</value></data>
|
||||
<data name="Oobe_CompleteTitle" xml:space="preserve"><value>란산 데스크톱에 오신 것을 환영합니다</value></data>
|
||||
<data name="Oobe_CompleteSubtitle" xml:space="preserve"><value>당신의 데스크톱, 한 면이 아닙니다</value></data>
|
||||
<data name="Oobe_MonetDesc" xml:space="preserve"><value>바탕 화면에서 테마 색상을 자동 추출하여 데스크톱과 완벽하게 융합</value></data>
|
||||
<data name="Oobe_MonetFromWallpaperDesc" xml:space="preserve"><value>현재 바탕 화면 색상을 분석하여 테마 생성</value></data>
|
||||
<data name="Oobe_MonetFromCustomImageDesc" xml:space="preserve"><value>이미지를 선택하여 색상 소스로 사용</value></data>
|
||||
<data name="Oobe_MonetDisabledDesc" xml:space="preserve"><value>고정된 프리셋 테마 색상 사용</value></data>
|
||||
<data name="Oobe_DataLocationDesc" xml:space="preserve"><value>앱 데이터를 저장할 위치를 결정하세요. 설정에서 언제든 변경할 수 있습니다.</value></data>
|
||||
<data name="Oobe_StartupDesc" xml:space="preserve"><value>이 옵션은 데스크톱 앱 설정에서 언제든 변경할 수 있습니다. 슬라이드 인은 Windows에서만 사용 가능합니다.</value></data>
|
||||
<data name="Oobe_ShowInTaskbarDesc" xml:space="preserve"><value>활성화하면 최소화 시 작업 표시줄에 항목이 유지됩니다. 비활성화하면 트레이 아이콘에 더 의존합니다.</value></data>
|
||||
<data name="Oobe_SlideTransitionDesc" xml:space="preserve"><value>화면 가장자리에서 슬라이드 인. 페이드 인과 상호 배타적.</value></data>
|
||||
<data name="Oobe_FadeTransitionDesc" xml:space="preserve"><value>슬라이드 인이 비활성화된 경우 권장.</value></data>
|
||||
<data name="Oobe_FusedDesktopDesc" xml:space="preserve"><value>퓨즈드 데스크톱과 세 손가락 스와이프 제스처를 활성화하여 에지 팝인 및 관련 실험 기능을 사용합니다(설정의 개발자 옵션과 동일).</value></data>
|
||||
<data name="Oobe_AutoStartDesc" xml:space="preserve"><value>이 런처를 현재 사용자의 시작 프로그램으로 등록합니다(설치 프로그램의 선택적 작업과 동일한 레지스트리 항목).</value></data>
|
||||
<data name="Oobe_AutoStartDescNonWindows" xml:space="preserve"><value>이 플랫폼에서는 설정만 저장됩니다. 자동 시작은 시스템의 앱 자동 시작 설정을 사용하세요.</value></data>
|
||||
<data name="Oobe_PrivacyDesc" xml:space="preserve"><value>원격 측정 프로그램 참여 여부를 선택하고 개인정보 정책을 확인</value></data>
|
||||
<data name="Oobe_CrashReportsDesc" xml:space="preserve"><value>앱 안정성 개선에 도움. 개인 식별 정보는 포함되지 않습니다</value></data>
|
||||
<data name="Oobe_UsageStatsDesc" xml:space="preserve"><value>기능 사용 현황을 파악하여 제품 경험 최적화</value></data>
|
||||
<data name="Oobe_PrivacyTrackingIdDesc" xml:space="preserve"><value>이 ID는 기기를 익명으로 식별하는 데 사용되며 개인 정보는 포함되지 않습니다</value></data>
|
||||
<data name="Oobe_PrivacyAgreementNote" xml:space="preserve"><value>원격 측정 기능을 활성화하려면 먼저 개인정보 정책을 읽고 동의해야 합니다. 원격 측정 데이터는 앱 안정성 개선 및 제품 경험 최적화에만 사용되며 개인 식별 정보는 포함되지 않습니다.</value></data>
|
||||
<data name="Oobe_TypingAppName" xml:space="preserve"><value>란산 데스크톱 LanMountain Desktop</value></data>
|
||||
<data name="Oobe_TypingNextGen" xml:space="preserve"><value>차세대</value></data>
|
||||
<data name="Oobe_TypingDashboard" xml:space="preserve"><value>인터랙티브 대시보드</value></data>
|
||||
<data name="Oobe_MigrationDetected" xml:space="preserve"><value>기존 데이터가 감지되었습니다. 휴대용 모드 선택 시 자동으로 마이그레이션됩니다.</value></data>
|
||||
<data name="Migration_Title" xml:space="preserve"><value>란산 데스크톱 - 버전 마이그레이션</value></data>
|
||||
<data name="Migration_DetectedOldVersion" xml:space="preserve"><value>이전 버전 감지됨</value></data>
|
||||
<data name="Migration_DetectedDesc" xml:space="preserve"><value>시스템에 이전 버전의 란산 데스크톱(0.8.4)이 감지되었습니다. 충돌을 방지하기 위해 제거를 권장합니다.</value></data>
|
||||
<data name="Migration_Version" xml:space="preserve"><value>버전: </value></data>
|
||||
<data name="Migration_Location" xml:space="preserve"><value>위치: </value></data>
|
||||
<data name="Migration_Type" xml:space="preserve"><value>유형: </value></data>
|
||||
<data name="Migration_Installed" xml:space="preserve"><value>설치 버전</value></data>
|
||||
<data name="Migration_UninstallNote" xml:space="preserve"><value>이전 버전을 제거해도 새 버전에는 영향을 미치지 않습니다. 개인 데이터는 보존됩니다.</value></data>
|
||||
<data name="Migration_ButtonViewLocation" xml:space="preserve"><value>위치 보기</value></data>
|
||||
<data name="Migration_ButtonSkip" xml:space="preserve"><value>나중에</value></data>
|
||||
<data name="Migration_ButtonUninstall" xml:space="preserve"><value>이전 버전 제거</value></data>
|
||||
<data name="Migration_Portable" xml:space="preserve"><value>휴대용 버전</value></data>
|
||||
<data name="Migration_Unknown" xml:space="preserve"><value>알 수 없음</value></data>
|
||||
<data name="Migration_DetectedDescFormat" xml:space="preserve"><value>시스템에 이전 버전의 란산 데스크톱({0})이 감지되었습니다. 새 버전은 완전히 새로운 아키텍처를 사용합니다. 더 나은 경험을 위해 이전 버전 제거를 권장합니다.</value></data>
|
||||
<data name="Privacy_Title" xml:space="preserve"><value>란산 데스크톱 원격 측정 개인정보 데이터 수집 동의서</value></data>
|
||||
<data name="Privacy_Header" xml:space="preserve"><value>란산 데스크톱 원격 측정 개인정보 데이터 수집 동의서</value></data>
|
||||
<data name="Privacy_Description" xml:space="preserve"><value>데이터의 수집, 사용 및 보호 방법을 이해하기 위해 다음 동의서를 주의 깊게 읽어주세요</value></data>
|
||||
<data name="Privacy_ButtonClose" xml:space="preserve"><value>닫기</value></data>
|
||||
<data name="DevDebug_Title" xml:space="preserve"><value>개발자 디버그 창</value></data>
|
||||
<data name="DevDebug_Splash" xml:space="preserve"><value>시작 화면</value></data>
|
||||
<data name="DevDebug_Error" xml:space="preserve"><value>오류 페이지</value></data>
|
||||
<data name="DevDebug_Update" xml:space="preserve"><value>업데이트 페이지</value></data>
|
||||
<data name="DevDebug_Oobe" xml:space="preserve"><value>OOBE 페이지</value></data>
|
||||
<data name="DevDebug_DataLocation" xml:space="preserve"><value>데이터 위치</value></data>
|
||||
<data name="DevDebug_EnableFeature" xml:space="preserve"><value>기능 활성화</value></data>
|
||||
<data name="DevDebug_Open" xml:space="preserve"><value>열기</value></data>
|
||||
<data name="DevDebug_SetAllViewMode" xml:space="preserve"><value>모두 보기 모드로 설정</value></data>
|
||||
<data name="DevDebug_SetAllFunctionMode" xml:space="preserve"><value>모두 기능 모드로 설정</value></data>
|
||||
<data name="DevDebug_Close" xml:space="preserve"><value>닫기</value></data>
|
||||
<data name="Coordinator_SlowDeviceMessage" xml:space="preserve"><value>장치가 느려 여전히 시작 중입니다. 잠시 기다려주세요.</value></data>
|
||||
<data name="Coordinator_RunningHostMessage" xml:space="preserve"><value>데스크톱 프로세스가 여전히 실행 중입니다. 런처는 계속 대기하며 다시 시작하지 않습니다.</value></data>
|
||||
<data name="Preview_SplashInitializing" xml:space="preserve"><value>초기화 중...</value></data>
|
||||
<data name="Preview_SplashCheckingUpdates" xml:space="preserve"><value>업데이트 확인 중...</value></data>
|
||||
<data name="Preview_SplashCheckingPlugins" xml:space="preserve"><value>플러그인 확인 중...</value></data>
|
||||
<data name="Preview_SplashLaunchingHost" xml:space="preserve"><value>호스트 시작 중...</value></data>
|
||||
<data name="Preview_SplashReady" xml:space="preserve"><value>준비 완료</value></data>
|
||||
<data name="Preview_ErrorMessage" xml:space="preserve"><value>[미리보기] 런처 오류 창 미리보기입니다.</value></data>
|
||||
<data name="Preview_UpdateProcessing" xml:space="preserve"><value>{0} 처리 중...</value></data>
|
||||
<data name="Preview_ActivationConnecting" xml:space="preserve"><value>활성 런처에 연결 중...</value></data>
|
||||
</root>
|
||||
219
LanMountainDesktop.Launcher/Resources/Strings.resx
Normal file
219
LanMountainDesktop.Launcher/Resources/Strings.resx
Normal file
@@ -0,0 +1,219 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
<xsd:attribute name="type" type="xsd:string" use="optional" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" use="optional" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
|
||||
<resheader name="version"><value>2.0</value></resheader>
|
||||
<resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms</value></resheader>
|
||||
<resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms</value></resheader>
|
||||
<data name="Splash_Title" xml:space="preserve"><value>阑山桌面</value></data>
|
||||
<data name="Splash_AppName" xml:space="preserve"><value>阑山桌面</value></data>
|
||||
<data name="Splash_StatusInitializing" xml:space="preserve"><value>正在初始化...</value></data>
|
||||
<data name="Splash_DebugPreview" xml:space="preserve"><value>[调试模式] 启动画面预览</value></data>
|
||||
<data name="Error_Title" xml:space="preserve"><value>阑山桌面</value></data>
|
||||
<data name="Error_TitleCannotConfirm" xml:space="preserve"><value>启动器无法确认启动状态</value></data>
|
||||
<data name="Error_MessageNotReached" xml:space="preserve"><value>阑山桌面未达到预期的启动状态。</value></data>
|
||||
<data name="Error_SuggestionTitle" xml:space="preserve"><value>启动恢复</value></data>
|
||||
<data name="Error_SuggestionMessage" xml:space="preserve"><value>您可以检查日志、等待当前进程或激活正在运行的桌面实例。</value></data>
|
||||
<data name="Error_DiagnosticHeader" xml:space="preserve"><value>诊断详情</value></data>
|
||||
<data name="Error_ButtonOpenLogs" xml:space="preserve"><value>打开日志</value></data>
|
||||
<data name="Error_ButtonCopy" xml:space="preserve"><value>复制</value></data>
|
||||
<data name="Error_ButtonWait" xml:space="preserve"><value>等待</value></data>
|
||||
<data name="Error_ButtonExit" xml:space="preserve"><value>退出</value></data>
|
||||
<data name="Error_ButtonRetry" xml:space="preserve"><value>重试</value></data>
|
||||
<data name="Error_ButtonActivate" xml:space="preserve"><value>激活</value></data>
|
||||
<data name="Error_DebugTitle" xml:space="preserve"><value>[调试] 启动器错误</value></data>
|
||||
<data name="Error_HostNotFoundTitle" xml:space="preserve"><value>启动器找不到桌面可执行文件</value></data>
|
||||
<data name="Error_HostNotFoundMessage" xml:space="preserve"><value>在调试模式下选择另一个可执行文件、检查日志,或在修复部署路径后重试。</value></data>
|
||||
<data name="Error_GenericRetryMessage" xml:space="preserve"><value>检查日志后重试,等待上一次启动尝试完全结束。</value></data>
|
||||
<data name="Error_GenericNoRetryMessage" xml:space="preserve"><value>检查日志或退出。旧进程仍在运行时,启动器不会创建新的桌面进程。</value></data>
|
||||
<data name="Error_PendingTitle" xml:space="preserve"><value>启动仍在进行中</value></data>
|
||||
<data name="Error_PendingMessage" xml:space="preserve"><value>桌面进程仍在运行,启动器不会启动第二个实例。</value></data>
|
||||
<data name="Error_PendingMessageWithPid" xml:space="preserve"><value>桌面进程仍在运行,启动器不会启动第二个实例。当前主进程 PID: {0}。</value></data>
|
||||
<data name="MultiInstance_Title" xml:space="preserve"><value>阑山桌面</value></data>
|
||||
<data name="MultiInstance_AlreadyRunning" xml:space="preserve"><value>阑山桌面已在运行</value></data>
|
||||
<data name="MultiInstance_AlreadyRunningMessage" xml:space="preserve"><value>启动器检测到已存在的桌面实例,未启动新进程。</value></data>
|
||||
<data name="MultiInstance_RepeatedLaunchTitle" xml:space="preserve"><value>重复启动</value></data>
|
||||
<data name="MultiInstance_RepeatedLaunchMessage" xml:space="preserve"><value>您当前的设置为显示此提示而不自动打开桌面。</value></data>
|
||||
<data name="MultiInstance_NoSecondProcess" xml:space="preserve"><value>未创建第二个主进程。</value></data>
|
||||
<data name="MultiInstance_ButtonCopy" xml:space="preserve"><value>复制</value></data>
|
||||
<data name="MultiInstance_ButtonClose" xml:space="preserve"><value>关闭</value></data>
|
||||
<data name="MultiInstance_ButtonOpenDesktop" xml:space="preserve"><value>打开桌面</value></data>
|
||||
<data name="MultiInstance_DetailsFormat" xml:space="preserve"><value>现有主进程 PID: {0} Shell 状态: {1} 未创建第二个主进程。</value></data>
|
||||
<data name="DataLocation_Title" xml:space="preserve"><value>选择数据保存位置</value></data>
|
||||
<data name="DataLocation_ChooseLocation" xml:space="preserve"><value>选择数据保存位置</value></data>
|
||||
<data name="DataLocation_ChooseLocationDesc" xml:space="preserve"><value>选择启动器和桌面数据的存储位置。您可以稍后在设置中更改。</value></data>
|
||||
<data name="DataLocation_NotWritable" xml:space="preserve"><value>应用目录不可写入</value></data>
|
||||
<data name="DataLocation_NotWritableDesc" xml:space="preserve"><value>当前安装目录需要管理员权限才能写入。数据将存储在系统用户目录中。</value></data>
|
||||
<data name="DataLocation_SystemProfile" xml:space="preserve"><value>保存在系统用户目录(推荐)</value></data>
|
||||
<data name="DataLocation_SystemProfileDesc" xml:space="preserve"><value>数据与当前 Windows 用户绑定,在应用重新安装和更新后保持完整。</value></data>
|
||||
<data name="DataLocation_Portable" xml:space="preserve"><value>保存在应用安装目录(便携模式)</value></data>
|
||||
<data name="DataLocation_PortableDesc" xml:space="preserve"><value>适用于便携安装。整个应用文件夹可以连同数据一起移动到另一台机器。</value></data>
|
||||
<data name="DataLocation_ButtonCancel" xml:space="preserve"><value>取消</value></data>
|
||||
<data name="DataLocation_ButtonConfirm" xml:space="preserve"><value>确认</value></data>
|
||||
<data name="DataLocation_MigrateWarning" xml:space="preserve"><value>检测到已有的系统数据。选择便携模式将自动迁移当前数据。</value></data>
|
||||
<data name="Loading_Title" xml:space="preserve"><value>阑山桌面 - 加载详情</value></data>
|
||||
<data name="Loading_StartingDesktop" xml:space="preserve"><value>正在启动阑山桌面</value></data>
|
||||
<data name="Loading_StatusInitializing" xml:space="preserve"><value>正在初始化...</value></data>
|
||||
<data name="Loading_StatusPreparing" xml:space="preserve"><value>正在准备组件</value></data>
|
||||
<data name="Loading_LoadingItems" xml:space="preserve"><value>加载项目</value></data>
|
||||
<data name="Loading_Done" xml:space="preserve"><value>完成</value></data>
|
||||
<data name="Loading_ErrorOccurred" xml:space="preserve"><value>加载时发生错误。</value></data>
|
||||
<data name="Loading_ButtonDetails" xml:space="preserve"><value>详情</value></data>
|
||||
<data name="Loading_ButtonCancel" xml:space="preserve"><value>取消</value></data>
|
||||
<data name="Loading_StageReady" xml:space="preserve"><value>准备就绪</value></data>
|
||||
<data name="Loading_ItemPlugin" xml:space="preserve"><value>正在加载插件...</value></data>
|
||||
<data name="Loading_ItemComponent" xml:space="preserve"><value>正在加载组件...</value></data>
|
||||
<data name="Loading_ItemResource" xml:space="preserve"><value>正在加载资源...</value></data>
|
||||
<data name="Loading_ItemData" xml:space="preserve"><value>正在加载数据...</value></data>
|
||||
<data name="Loading_ItemDownload" xml:space="preserve"><value>正在下载...</value></data>
|
||||
<data name="Loading_ItemProcess" xml:space="preserve"><value>正在处理...</value></data>
|
||||
<data name="Loading_ItemComplete" xml:space="preserve"><value>完成</value></data>
|
||||
<data name="Loading_TypePlugin" xml:space="preserve"><value>插件</value></data>
|
||||
<data name="Loading_TypeComponent" xml:space="preserve"><value>组件</value></data>
|
||||
<data name="Loading_TypeResource" xml:space="preserve"><value>资源</value></data>
|
||||
<data name="Loading_TypeData" xml:space="preserve"><value>数据</value></data>
|
||||
<data name="Loading_TypeNetwork" xml:space="preserve"><value>网络</value></data>
|
||||
<data name="Loading_TypeSettings" xml:space="preserve"><value>设置</value></data>
|
||||
<data name="Loading_TypeSystem" xml:space="preserve"><value>系统</value></data>
|
||||
<data name="Loading_TypeOther" xml:space="preserve"><value>其他</value></data>
|
||||
<data name="Update_Title" xml:space="preserve"><value>阑山桌面 - 更新</value></data>
|
||||
<data name="Update_AppName" xml:space="preserve"><value>阑山桌面</value></data>
|
||||
<data name="Update_StatusUpdate" xml:space="preserve"><value>更新</value></data>
|
||||
<data name="Update_StatusUpdating" xml:space="preserve"><value>正在更新,请稍候...</value></data>
|
||||
<data name="Update_Complete" xml:space="preserve"><value>更新完成</value></data>
|
||||
<data name="Update_Failed" xml:space="preserve"><value>更新失败</value></data>
|
||||
<data name="Update_FailedMessage" xml:space="preserve"><value>更新过程中发生错误</value></data>
|
||||
<data name="Update_DebugTitle" xml:space="preserve"><value>[调试模式] 更新页面</value></data>
|
||||
<data name="Update_DebugMessage" xml:space="preserve"><value>预览更新进度界面</value></data>
|
||||
<data name="Update_Verifying" xml:space="preserve"><value>正在验证更新...</value></data>
|
||||
<data name="Update_ApplyingPlugins" xml:space="preserve"><value>正在应用插件升级...</value></data>
|
||||
<data name="Update_CleaningUp" xml:space="preserve"><value>正在清理旧部署...</value></data>
|
||||
<data name="DebugDebug_Title" xml:space="preserve"><value>调试模式</value></data>
|
||||
<data name="DebugDebug_SettingsTitle" xml:space="preserve"><value>调试设置</value></data>
|
||||
<data name="DebugDebug_DevMode" xml:space="preserve"><value>开发模式</value></data>
|
||||
<data name="DebugDebug_DevModeDesc" xml:space="preserve"><value>启用后自动扫描开发目录</value></data>
|
||||
<data name="DebugDebug_On" xml:space="preserve"><value>开</value></data>
|
||||
<data name="DebugDebug_Off" xml:space="preserve"><value>关</value></data>
|
||||
<data name="DebugDebug_AppPath" xml:space="preserve"><value>应用路径</value></data>
|
||||
<data name="DebugDebug_NotSelected" xml:space="preserve"><value>未选择</value></data>
|
||||
<data name="DebugDebug_Browse" xml:space="preserve"><value>浏览...</value></data>
|
||||
<data name="DebugDebug_Warning" xml:space="preserve"><value>此功能仅供开发人员使用</value></data>
|
||||
<data name="DebugDebug_ButtonCancel" xml:space="preserve"><value>取消</value></data>
|
||||
<data name="DebugDebug_ButtonOk" xml:space="preserve"><value>确定</value></data>
|
||||
<data name="DebugDebug_SelectExeDialog" xml:space="preserve"><value>选择阑山桌面主程序可执行文件</value></data>
|
||||
<data name="Oobe_Title" xml:space="preserve"><value>欢迎使用阑山桌面</value></data>
|
||||
<data name="Oobe_WelcomeTitle" xml:space="preserve"><value>欢迎使用阑山桌面</value></data>
|
||||
<data name="Oobe_WelcomeSubtitle" xml:space="preserve"><value>你的桌面,不止一面</value></data>
|
||||
<data name="Oobe_ButtonGetStarted" xml:space="preserve"><value>开始使用</value></data>
|
||||
<data name="Oobe_AppearanceTitle" xml:space="preserve"><value>个性化你的桌面</value></data>
|
||||
<data name="Oobe_AppearanceDesc" xml:space="preserve"><value>选择你喜欢的主题样式,可随时在设置中更改</value></data>
|
||||
<data name="Oobe_AppearanceMode" xml:space="preserve"><value>外观模式</value></data>
|
||||
<data name="Oobe_LightMode" xml:space="preserve"><value>浅色模式</value></data>
|
||||
<data name="Oobe_DarkMode" xml:space="preserve"><value>深色模式</value></data>
|
||||
<data name="Oobe_ThemeColor" xml:space="preserve"><value>主题色</value></data>
|
||||
<data name="Oobe_MonetSource" xml:space="preserve"><value>莫奈取色来源</value></data>
|
||||
<data name="Oobe_MonetFromWallpaper" xml:space="preserve"><value>从桌面壁纸取色</value></data>
|
||||
<data name="Oobe_MonetFromCustomImage" xml:space="preserve"><value>自定义图片取色</value></data>
|
||||
<data name="Oobe_MonetDisabled" xml:space="preserve"><value>不使用莫奈取色</value></data>
|
||||
<data name="Oobe_DataLocationTitle" xml:space="preserve"><value>选择数据保存位置</value></data>
|
||||
<data name="Oobe_SystemProfile" xml:space="preserve"><value>保存在系统用户目录(推荐)</value></data>
|
||||
<data name="Oobe_SystemProfileDesc" xml:space="preserve"><value>数据与当前 Windows 用户绑定,在应用重新安装和更新后保持完整。</value></data>
|
||||
<data name="Oobe_Portable" xml:space="preserve"><value>保存在应用安装目录(便携模式)</value></data>
|
||||
<data name="Oobe_PortableDesc" xml:space="preserve"><value>适用于便携安装。整个应用文件夹可以连同数据一起移动到另一台机器。</value></data>
|
||||
<data name="Oobe_NotWritable" xml:space="preserve"><value>无法保存到应用目录</value></data>
|
||||
<data name="Oobe_NotWritableDesc" xml:space="preserve"><value>当前安装目录需要管理员权限才能写入。数据将存储在系统用户目录中。</value></data>
|
||||
<data name="Oobe_StartupTitle" xml:space="preserve"><value>启动与展示</value></data>
|
||||
<data name="Oobe_ShowInTaskbar" xml:space="preserve"><value>在任务栏显示主桌面窗口</value></data>
|
||||
<data name="Oobe_SlideTransition" xml:space="preserve"><value>以滑动方式显示主窗口</value></data>
|
||||
<data name="Oobe_FadeTransition" xml:space="preserve"><value>启动时使用淡入过渡</value></data>
|
||||
<data name="Oobe_FusedDesktop" xml:space="preserve"><value>融合桌面与弹入手势</value></data>
|
||||
<data name="Oobe_AutoStart" xml:space="preserve"><value>登录 Windows 时自动启动阑山桌面</value></data>
|
||||
<data name="Oobe_PrivacyTitle" xml:space="preserve"><value>信息与隐私</value></data>
|
||||
<data name="Oobe_CrashReports" xml:space="preserve"><value>发送匿名崩溃报告</value></data>
|
||||
<data name="Oobe_UsageStats" xml:space="preserve"><value>发送匿名使用统计</value></data>
|
||||
<data name="Oobe_PrivacyTrackingId" xml:space="preserve"><value>隐私追踪 ID</value></data>
|
||||
<data name="Oobe_Agree" xml:space="preserve"><value>同意</value></data>
|
||||
<data name="Oobe_PrivacyPolicyLink" xml:space="preserve"><value>《阑山桌面遥测隐私数据收集协议》</value></data>
|
||||
<data name="Oobe_ButtonBack" xml:space="preserve"><value>返回</value></data>
|
||||
<data name="Oobe_ButtonNext" xml:space="preserve"><value>下一步</value></data>
|
||||
<data name="Oobe_CompleteTitle" xml:space="preserve"><value>欢迎使用阑山桌面</value></data>
|
||||
<data name="Oobe_CompleteSubtitle" xml:space="preserve"><value>你的桌面,不止一面</value></data>
|
||||
<data name="Oobe_MonetDesc" xml:space="preserve"><value>从壁纸自动提取主题色,让界面与桌面完美融合</value></data>
|
||||
<data name="Oobe_MonetFromWallpaperDesc" xml:space="preserve"><value>自动分析当前壁纸颜色生成主题</value></data>
|
||||
<data name="Oobe_MonetFromCustomImageDesc" xml:space="preserve"><value>选择一张图片作为取色来源</value></data>
|
||||
<data name="Oobe_MonetDisabledDesc" xml:space="preserve"><value>使用固定的预设主题色</value></data>
|
||||
<data name="Oobe_DataLocationDesc" xml:space="preserve"><value>决定将应用数据保存在哪里,可随时在设置中更改</value></data>
|
||||
<data name="Oobe_StartupDesc" xml:space="preserve"><value>这些选项可随时在桌面应用的「设置」中更改。主窗口滑动入场仅在 Windows 上可用。</value></data>
|
||||
<data name="Oobe_ShowInTaskbarDesc" xml:space="preserve"><value>开启后最小化时可在任务栏保留条目;关闭则更多依赖托盘图标。</value></data>
|
||||
<data name="Oobe_SlideTransitionDesc" xml:space="preserve"><value>自屏幕边缘滑入;与「淡入」二选一。</value></data>
|
||||
<data name="Oobe_FadeTransitionDesc" xml:space="preserve"><value>在未启用滑动入场时建议使用。</value></data>
|
||||
<data name="Oobe_FusedDesktopDesc" xml:space="preserve"><value>同时启用融合桌面与三指滑动手势,以便使用边缘弹入与相关实验特性(与设置中开发者选项一致)。</value></data>
|
||||
<data name="Oobe_AutoStartDesc" xml:space="preserve"><value>通过当前用户的启动项注册本启动器(与安装程序可选任务使用同一注册表项)。</value></data>
|
||||
<data name="Oobe_AutoStartDescNonWindows" xml:space="preserve"><value>当前平台仅保存偏好;是否随系统自启动请使用系统提供的应用自启动设置。</value></data>
|
||||
<data name="Oobe_PrivacyDesc" xml:space="preserve"><value>选择是否参与遥测计划,查看隐私政策</value></data>
|
||||
<data name="Oobe_CrashReportsDesc" xml:space="preserve"><value>帮助改进应用稳定性,不包含个人身份信息</value></data>
|
||||
<data name="Oobe_UsageStatsDesc" xml:space="preserve"><value>帮助了解功能使用情况,优化产品体验</value></data>
|
||||
<data name="Oobe_PrivacyTrackingIdDesc" xml:space="preserve"><value>此 ID 用于匿名标识您的设备,不包含任何个人信息</value></data>
|
||||
<data name="Oobe_PrivacyAgreementNote" xml:space="preserve"><value>您必须阅读并同意隐私协议后,才能开启遥测功能。遥测数据仅用于改进应用稳定性和优化产品体验,不包含任何个人身份信息。</value></data>
|
||||
<data name="Oobe_TypingAppName" xml:space="preserve"><value>阑山桌面 LanMountain Desktop</value></data>
|
||||
<data name="Oobe_TypingNextGen" xml:space="preserve"><value>下一代</value></data>
|
||||
<data name="Oobe_TypingDashboard" xml:space="preserve"><value>互动信息看板</value></data>
|
||||
<data name="Oobe_MigrationDetected" xml:space="preserve"><value>检测到现有数据,选择便携模式时将自动迁移。</value></data>
|
||||
<data name="Migration_Title" xml:space="preserve"><value>阑山桌面 - 版本迁移</value></data>
|
||||
<data name="Migration_DetectedOldVersion" xml:space="preserve"><value>检测到旧版本</value></data>
|
||||
<data name="Migration_DetectedDesc" xml:space="preserve"><value>检测到您的系统中安装了旧版本的阑山桌面(0.8.4),建议卸载以避免冲突。</value></data>
|
||||
<data name="Migration_Version" xml:space="preserve"><value>版本:</value></data>
|
||||
<data name="Migration_Location" xml:space="preserve"><value>位置:</value></data>
|
||||
<data name="Migration_Type" xml:space="preserve"><value>类型:</value></data>
|
||||
<data name="Migration_Installed" xml:space="preserve"><value>安装版</value></data>
|
||||
<data name="Migration_UninstallNote" xml:space="preserve"><value>卸载旧版本不会影响新版本的使用,您的个人数据将保留。</value></data>
|
||||
<data name="Migration_ButtonViewLocation" xml:space="preserve"><value>查看位置</value></data>
|
||||
<data name="Migration_ButtonSkip" xml:space="preserve"><value>暂不处理</value></data>
|
||||
<data name="Migration_ButtonUninstall" xml:space="preserve"><value>卸载旧版本</value></data>
|
||||
<data name="Migration_Portable" xml:space="preserve"><value>便携版</value></data>
|
||||
<data name="Migration_Unknown" xml:space="preserve"><value>未知</value></data>
|
||||
<data name="Migration_DetectedDescFormat" xml:space="preserve"><value>检测到您的系统中安装了旧版本的阑山桌面({0})。新版本采用了全新的架构,建议卸载旧版本以获得更好的体验。</value></data>
|
||||
<data name="Privacy_Title" xml:space="preserve"><value>阑山桌面遥测隐私数据收集协议</value></data>
|
||||
<data name="Privacy_Header" xml:space="preserve"><value>阑山桌面遥测隐私数据收集协议</value></data>
|
||||
<data name="Privacy_Description" xml:space="preserve"><value>请仔细阅读以下协议内容,了解我们如何收集、使用和保护您的数据</value></data>
|
||||
<data name="Privacy_ButtonClose" xml:space="preserve"><value>关闭</value></data>
|
||||
<data name="DevDebug_Title" xml:space="preserve"><value>开发调试窗口</value></data>
|
||||
<data name="DevDebug_Splash" xml:space="preserve"><value>启动画面</value></data>
|
||||
<data name="DevDebug_Error" xml:space="preserve"><value>错误页面</value></data>
|
||||
<data name="DevDebug_Update" xml:space="preserve"><value>更新页面</value></data>
|
||||
<data name="DevDebug_Oobe" xml:space="preserve"><value>OOBE页面</value></data>
|
||||
<data name="DevDebug_DataLocation" xml:space="preserve"><value>数据位置选择</value></data>
|
||||
<data name="DevDebug_EnableFeature" xml:space="preserve"><value>启用功能</value></data>
|
||||
<data name="DevDebug_Open" xml:space="preserve"><value>打开</value></data>
|
||||
<data name="DevDebug_SetAllViewMode" xml:space="preserve"><value>全部设为查看模式</value></data>
|
||||
<data name="DevDebug_SetAllFunctionMode" xml:space="preserve"><value>全部设为功能模式</value></data>
|
||||
<data name="DevDebug_Close" xml:space="preserve"><value>关闭</value></data>
|
||||
<data name="Coordinator_SlowDeviceMessage" xml:space="preserve"><value>设备较慢,仍在启动,请稍候。</value></data>
|
||||
<data name="Coordinator_RunningHostMessage" xml:space="preserve"><value>桌面主进程仍在运行,Launcher 会继续等待,不会重复启动。</value></data>
|
||||
<data name="Preview_SplashInitializing" xml:space="preserve"><value>正在初始化...</value></data>
|
||||
<data name="Preview_SplashCheckingUpdates" xml:space="preserve"><value>正在检查更新...</value></data>
|
||||
<data name="Preview_SplashCheckingPlugins" xml:space="preserve"><value>正在检查插件...</value></data>
|
||||
<data name="Preview_SplashLaunchingHost" xml:space="preserve"><value>正在启动主程序...</value></data>
|
||||
<data name="Preview_SplashReady" xml:space="preserve"><value>准备就绪</value></data>
|
||||
<data name="Preview_ErrorMessage" xml:space="preserve"><value>[预览] 这是启动器错误窗口预览。</value></data>
|
||||
<data name="Preview_UpdateProcessing" xml:space="preserve"><value>正在处理 {0}...</value></data>
|
||||
<data name="Preview_ActivationConnecting" xml:space="preserve"><value>正在连接到活跃的启动器...</value></data>
|
||||
</root>
|
||||
@@ -5,6 +5,11 @@ internal static class AirAppInstanceKey
|
||||
public static string Build(string appId, string? sourceComponentId, string? sourcePlacementId)
|
||||
{
|
||||
var normalizedAppId = Normalize(appId, "unknown");
|
||||
if (string.Equals(normalizedAppId, "world-clock", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return $"{normalizedAppId}:clock-suite:global";
|
||||
}
|
||||
|
||||
var normalizedComponentId = Normalize(sourceComponentId, "none");
|
||||
var normalizedPlacementId = Normalize(sourcePlacementId, "none");
|
||||
return $"{normalizedAppId}:{normalizedComponentId}:{normalizedPlacementId}";
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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<string>(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"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Launcher.Models;
|
||||
using LanMountainDesktop.Launcher.Resources;
|
||||
using LanMountainDesktop.Launcher.Services.Ipc;
|
||||
using LanMountainDesktop.Launcher.Views;
|
||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
@@ -13,8 +14,8 @@ internal sealed class LauncherFlowCoordinator
|
||||
{
|
||||
private static readonly TimeSpan StartupSoftTimeout = TimeSpan.FromSeconds(10);
|
||||
private static readonly TimeSpan StartupHardTimeout = TimeSpan.FromSeconds(30);
|
||||
private const string SoftTimeoutStatusMessage = "设备较慢,仍在启动,请稍候。";
|
||||
private const string SoftTimeoutDetailsMessage = "桌面主进程仍在运行,Launcher 会继续等待,不会重复启动。";
|
||||
private static readonly string SoftTimeoutStatusMessage = Strings.Coordinator_SlowDeviceMessage;
|
||||
private static readonly string SoftTimeoutDetailsMessage = Strings.Coordinator_RunningHostMessage;
|
||||
|
||||
private readonly CommandContext _context;
|
||||
private readonly DeploymentLocator _deploymentLocator;
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="520"
|
||||
d:DesignHeight="480"
|
||||
x:Class="LanMountainDesktop.Launcher.Views.DataLocationPromptWindow"
|
||||
x:DataType="views:DataLocationPromptWindow"
|
||||
Title="Choose Data Location"
|
||||
x:CompileBindings="False"
|
||||
Title="{x:Static res:Strings.DataLocation_Title}"
|
||||
Width="520"
|
||||
Height="480"
|
||||
CanResize="False"
|
||||
@@ -24,11 +26,11 @@
|
||||
</Grid.RenderTransform>
|
||||
<Grid Margin="36" RowDefinitions="Auto,*,Auto">
|
||||
<StackPanel Grid.Row="0" Spacing="8" Margin="0,0,0,20">
|
||||
<TextBlock Text="Choose Data Location"
|
||||
<TextBlock Text="{x:Static res:Strings.DataLocation_ChooseLocation}"
|
||||
FontSize="22"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="Choose where launcher and desktop data should be stored. You can change this later in settings."
|
||||
<TextBlock Text="{x:Static res:Strings.DataLocation_ChooseLocationDesc}"
|
||||
FontSize="13"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -45,7 +47,7 @@
|
||||
<fi:SymbolIcon Symbol="Important"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
|
||||
<TextBlock Text="App folder is not writable"
|
||||
<TextBlock Text="{x:Static res:Strings.DataLocation_NotWritable}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
|
||||
@@ -71,11 +73,11 @@
|
||||
GroupName="DataLocation"
|
||||
IsChecked="True" />
|
||||
<StackPanel Grid.Column="1" Spacing="4">
|
||||
<TextBlock Text="Store in the system user profile (Recommended)"
|
||||
<TextBlock Text="{x:Static res:Strings.DataLocation_SystemProfile}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="Data stays tied to the current Windows user and remains intact across app reinstalls and updates."
|
||||
<TextBlock Text="{x:Static res:Strings.DataLocation_SystemProfileDesc}"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -102,11 +104,11 @@
|
||||
GroupName="DataLocation"
|
||||
IsEnabled="False" />
|
||||
<StackPanel Grid.Column="1" Spacing="4">
|
||||
<TextBlock Text="Store next to the app"
|
||||
<TextBlock Text="{x:Static res:Strings.DataLocation_Portable}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="Useful for portable installs. The whole app folder can be moved to another machine together with its data."
|
||||
<TextBlock Text="{x:Static res:Strings.DataLocation_PortableDesc}"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -146,7 +148,7 @@
|
||||
Theme="{DynamicResource ButtonTheme}"
|
||||
IsVisible="False" />
|
||||
<Button x:Name="ConfirmButton"
|
||||
Content="Confirm"
|
||||
Content="{x:Static res:Strings.DataLocation_ButtonConfirm}"
|
||||
Theme="{DynamicResource AccentButtonTheme}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -7,6 +7,7 @@ using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using LanMountainDesktop.Launcher.Models;
|
||||
using LanMountainDesktop.Launcher.Resources;
|
||||
using LanMountainDesktop.Launcher.Services;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Views;
|
||||
@@ -106,7 +107,7 @@ internal partial class DataLocationPromptWindow : Window
|
||||
|
||||
if (migrationInfoText is not null && hasExistingData)
|
||||
{
|
||||
migrationInfoText.Text = "Existing system data was detected. Choosing portable mode will migrate the current data automatically.";
|
||||
migrationInfoText.Text = Strings.DataLocation_MigrateWarning;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:LanMountainDesktop.Launcher.ViewModels"
|
||||
xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources"
|
||||
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="600"
|
||||
x:Class="LanMountainDesktop.Launcher.Views.DevDebugWindow"
|
||||
x:DataType="vm:DevDebugWindowViewModel"
|
||||
Title="开发调试窗口 - Launcher"
|
||||
x:CompileBindings="False"
|
||||
Title="{x:Static res:Strings.DevDebug_Title}"
|
||||
Width="500"
|
||||
Height="600"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
@@ -43,7 +45,7 @@
|
||||
Padding="15">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="🚀 启动画面 (SplashWindow)"
|
||||
<TextBlock Text="{x:Static res:Strings.DevDebug_Splash}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="14" />
|
||||
<TextBlock Text="显示启动进度和状态"
|
||||
@@ -52,11 +54,11 @@
|
||||
Margin="0,3,0,0" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Spacing="8">
|
||||
<ToggleSwitch Content="启用功能"
|
||||
<ToggleSwitch Content="{x:Static res:Strings.DevDebug_EnableFeature}"
|
||||
IsChecked="{Binding IsSplashEnabled}"
|
||||
OnContent="功能"
|
||||
OffContent="查看" />
|
||||
<Button Content="打开"
|
||||
<Button Content="{x:Static res:Strings.DevDebug_Open}"
|
||||
Command="{Binding OpenSplashCommand}"
|
||||
HorizontalAlignment="Right" />
|
||||
</StackPanel>
|
||||
@@ -69,7 +71,7 @@
|
||||
Padding="15">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="❌ 错误页面 (ErrorWindow)"
|
||||
<TextBlock Text="{x:Static res:Strings.DevDebug_Error}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="14" />
|
||||
<TextBlock Text="显示错误信息和重试选项"
|
||||
@@ -78,11 +80,11 @@
|
||||
Margin="0,3,0,0" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Spacing="8">
|
||||
<ToggleSwitch Content="启用功能"
|
||||
<ToggleSwitch Content="{x:Static res:Strings.DevDebug_EnableFeature}"
|
||||
IsChecked="{Binding IsErrorEnabled}"
|
||||
OnContent="功能"
|
||||
OffContent="查看" />
|
||||
<Button Content="打开"
|
||||
<Button Content="{x:Static res:Strings.DevDebug_Open}"
|
||||
Command="{Binding OpenErrorCommand}"
|
||||
HorizontalAlignment="Right" />
|
||||
</StackPanel>
|
||||
@@ -95,7 +97,7 @@
|
||||
Padding="15">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="⬆️ 更新页面 (UpdateWindow)"
|
||||
<TextBlock Text="{x:Static res:Strings.DevDebug_Update}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="14" />
|
||||
<TextBlock Text="显示更新进度和状态"
|
||||
@@ -104,11 +106,11 @@
|
||||
Margin="0,3,0,0" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Spacing="8">
|
||||
<ToggleSwitch Content="启用功能"
|
||||
<ToggleSwitch Content="{x:Static res:Strings.DevDebug_EnableFeature}"
|
||||
IsChecked="{Binding IsUpdateEnabled}"
|
||||
OnContent="功能"
|
||||
OffContent="查看" />
|
||||
<Button Content="打开"
|
||||
<Button Content="{x:Static res:Strings.DevDebug_Open}"
|
||||
Command="{Binding OpenUpdateCommand}"
|
||||
HorizontalAlignment="Right" />
|
||||
</StackPanel>
|
||||
@@ -121,7 +123,7 @@
|
||||
Padding="15">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="👋 OOBE页面 (OobeWindow)"
|
||||
<TextBlock Text="{x:Static res:Strings.DevDebug_Oobe}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="14" />
|
||||
<TextBlock Text="首次运行引导页面"
|
||||
@@ -130,11 +132,11 @@
|
||||
Margin="0,3,0,0" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Spacing="8">
|
||||
<ToggleSwitch Content="启用功能"
|
||||
<ToggleSwitch Content="{x:Static res:Strings.DevDebug_EnableFeature}"
|
||||
IsChecked="{Binding IsOobeEnabled}"
|
||||
OnContent="功能"
|
||||
OffContent="查看" />
|
||||
<Button Content="打开"
|
||||
<Button Content="{x:Static res:Strings.DevDebug_Open}"
|
||||
Command="{Binding OpenOobeCommand}"
|
||||
HorizontalAlignment="Right" />
|
||||
</StackPanel>
|
||||
@@ -147,7 +149,7 @@
|
||||
Padding="15">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="📁 数据位置选择 (DataLocationPromptWindow)"
|
||||
<TextBlock Text="{x:Static res:Strings.DevDebug_DataLocation}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="14" />
|
||||
<TextBlock Text="选择数据保存位置"
|
||||
@@ -156,11 +158,11 @@
|
||||
Margin="0,3,0,0" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Spacing="8">
|
||||
<ToggleSwitch Content="启用功能"
|
||||
<ToggleSwitch Content="{x:Static res:Strings.DevDebug_EnableFeature}"
|
||||
IsChecked="{Binding IsDataLocationEnabled}"
|
||||
OnContent="功能"
|
||||
OffContent="查看" />
|
||||
<Button Content="打开"
|
||||
<Button Content="{x:Static res:Strings.DevDebug_Open}"
|
||||
Command="{Binding OpenDataLocationCommand}"
|
||||
HorizontalAlignment="Right" />
|
||||
</StackPanel>
|
||||
@@ -176,10 +178,10 @@
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="10"
|
||||
Margin="0,15">
|
||||
<Button Content="全部设为查看模式"
|
||||
<Button Content="{x:Static res:Strings.DevDebug_SetAllViewMode}"
|
||||
Command="{Binding SetAllViewOnlyCommand}"
|
||||
Background="{DynamicResource SystemControlBackgroundAltMediumBrush}" />
|
||||
<Button Content="全部设为功能模式"
|
||||
<Button Content="{x:Static res:Strings.DevDebug_SetAllFunctionMode}"
|
||||
Command="{Binding SetAllFunctionalCommand}"
|
||||
Background="{DynamicResource SystemControlHighlightAccentBrush}"
|
||||
Foreground="White" />
|
||||
@@ -197,7 +199,7 @@
|
||||
Opacity="0.8"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<Button Grid.Column="1"
|
||||
Content="关闭"
|
||||
Content="{x:Static res:Strings.DevDebug_Close}"
|
||||
Command="{Binding CloseCommand}"
|
||||
Padding="15,5" />
|
||||
</Grid>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using LanMountainDesktop.Launcher.Resources;
|
||||
using LanMountainDesktop.Launcher.Services;
|
||||
using LanMountainDesktop.Launcher.ViewModels;
|
||||
using LanMountainDesktop.Launcher.Views;
|
||||
@@ -62,12 +63,12 @@ public partial class DevDebugWindow : Window
|
||||
{
|
||||
// 查看模式:显示模拟错误
|
||||
errorWindow.SetDebugMode(true);
|
||||
errorWindow.SetErrorMessage("[调试模式] 这是一个模拟的错误消息,用于查看错误页面的样式和布局。");
|
||||
errorWindow.SetErrorMessage(Strings.Preview_ErrorMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 功能模式:显示真实错误
|
||||
errorWindow.SetErrorMessage("找不到阑山桌面应用程序。\n\n请检查应用安装是否完整。");
|
||||
errorWindow.SetErrorMessage(Strings.Error_HostNotFoundMessage);
|
||||
}
|
||||
|
||||
errorWindow.Show();
|
||||
@@ -160,7 +161,7 @@ public partial class DevDebugWindow : Window
|
||||
/// </summary>
|
||||
private async Task SimulateSplashProgress(SplashWindow splashWindow)
|
||||
{
|
||||
var stages = new[] { "初始化", "检查更新", "加载组件", "启动应用" };
|
||||
var stages = new[] { Strings.Preview_SplashInitializing, Strings.Preview_SplashCheckingUpdates, Strings.Preview_SplashCheckingPlugins, Strings.Preview_SplashLaunchingHost };
|
||||
var reporter = (ISplashStageReporter)splashWindow;
|
||||
|
||||
for (int i = 0; i < stages.Length; i++)
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
|
||||
xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="420"
|
||||
d:DesignHeight="320"
|
||||
x:Class="LanMountainDesktop.Launcher.Views.ErrorDebugWindow"
|
||||
x:DataType="views:ErrorDebugWindow"
|
||||
Title="调试模式"
|
||||
x:CompileBindings="False"
|
||||
Title="{x:Static res:Strings.DebugDebug_Title}"
|
||||
Width="420"
|
||||
Height="320"
|
||||
CanResize="False"
|
||||
@@ -23,7 +25,7 @@
|
||||
<Grid Margin="24" RowDefinitions="Auto,*,Auto">
|
||||
<!-- 标题 -->
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="调试设置"
|
||||
Text="{x:Static res:Strings.DebugDebug_SettingsTitle}"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
@@ -37,18 +39,18 @@
|
||||
Padding="16,12">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" VerticalAlignment="Center">
|
||||
<TextBlock Text="开发模式"
|
||||
<TextBlock Text="{x:Static res:Strings.DebugDebug_DevMode}"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="启用后自动扫描开发目录"
|
||||
<TextBlock Text="{x:Static res:Strings.DebugDebug_DevModeDesc}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Margin="0,2,0,0" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch x:Name="DevModeToggle"
|
||||
Grid.Column="1"
|
||||
OnContent="开"
|
||||
OffContent="关" />
|
||||
OnContent="{x:Static res:Strings.DebugDebug_On}"
|
||||
OffContent="{x:Static res:Strings.DebugDebug_Off}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -58,19 +60,19 @@
|
||||
Padding="16,12">
|
||||
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||
Text="应用路径"
|
||||
Text="{x:Static res:Strings.DebugDebug_AppPath}"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock x:Name="PathTextBlock"
|
||||
Grid.Row="1" Grid.Column="0"
|
||||
Text="未选择"
|
||||
Text="{x:Static res:Strings.DebugDebug_NotSelected}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Margin="0,4,12,0" />
|
||||
<Button x:Name="BrowseButton"
|
||||
Grid.Row="0" Grid.RowSpan="2" Grid.Column="1"
|
||||
Content="浏览..."
|
||||
Content="{x:Static res:Strings.DebugDebug_Browse}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -80,7 +82,7 @@
|
||||
CornerRadius="{DynamicResource ControlCornerRadius}"
|
||||
Padding="12,10"
|
||||
IsVisible="True">
|
||||
<TextBlock Text="此功能仅供开发人员使用"
|
||||
<TextBlock Text="{x:Static res:Strings.DebugDebug_Warning}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource SystemFillColorCautionBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
@@ -94,11 +96,11 @@
|
||||
Spacing="12"
|
||||
Margin="0,16,0,0">
|
||||
<Button x:Name="CancelButton"
|
||||
Content="取消"
|
||||
Content="{x:Static res:Strings.DebugDebug_ButtonCancel}"
|
||||
Width="80"
|
||||
Height="32" />
|
||||
<Button x:Name="OkButton"
|
||||
Content="确定"
|
||||
Content="{x:Static res:Strings.DebugDebug_ButtonOk}"
|
||||
Width="80"
|
||||
Height="32"
|
||||
Theme="{DynamicResource AccentButtonTheme}" />
|
||||
|
||||
@@ -2,6 +2,7 @@ using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Platform.Storage;
|
||||
using LanMountainDesktop.Launcher.Resources;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Views;
|
||||
|
||||
@@ -87,7 +88,7 @@ public partial class ErrorDebugWindow : Window
|
||||
|
||||
var options = new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Select LanMountainDesktop host executable",
|
||||
Title = Strings.DebugDebug_SelectExeDialog,
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter =
|
||||
[
|
||||
@@ -114,7 +115,7 @@ public partial class ErrorDebugWindow : Window
|
||||
{
|
||||
if (this.FindControl<TextBlock>("PathTextBlock") is { } pathTextBlock)
|
||||
{
|
||||
pathTextBlock.Text = string.IsNullOrEmpty(path) ? "Not selected" : path;
|
||||
pathTextBlock.Text = string.IsNullOrEmpty(path) ? Strings.DebugDebug_NotSelected : path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
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"
|
||||
x:Class="LanMountainDesktop.Launcher.Views.ErrorWindow"
|
||||
x:DataType="views:ErrorWindow"
|
||||
Title="LanMountain Desktop"
|
||||
x:CompileBindings="False"
|
||||
Title="{x:Static res:Strings.Error_Title}"
|
||||
Width="760"
|
||||
Height="460"
|
||||
MinWidth="640"
|
||||
@@ -46,14 +48,14 @@
|
||||
<StackPanel Grid.Column="1"
|
||||
Spacing="8">
|
||||
<TextBlock x:Name="TitleText"
|
||||
Text="Launcher could not confirm startup"
|
||||
Text="{x:Static res:Strings.Error_TitleCannotConfirm}"
|
||||
FontSize="22"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock x:Name="ErrorMessageText"
|
||||
Text="LanMountain Desktop did not reach the expected startup state."
|
||||
Text="{x:Static res:Strings.Error_MessageNotReached}"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
@@ -68,8 +70,8 @@
|
||||
IsOpen="True"
|
||||
IsClosable="False"
|
||||
Severity="Warning"
|
||||
Title="Startup recovery"
|
||||
Message="You can inspect logs, wait for the current process, or activate the running desktop instance.">
|
||||
Title="{x:Static res:Strings.Error_SuggestionTitle}"
|
||||
Message="{x:Static res:Strings.Error_SuggestionMessage}">
|
||||
<ui:FAInfoBar.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰊈"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
@@ -79,7 +81,7 @@
|
||||
<Expander Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Header="Diagnostic details"
|
||||
Header="{x:Static res:Strings.Error_DiagnosticHeader}"
|
||||
IsExpanded="True">
|
||||
<TextBox x:Name="ErrorDetailsTextBox"
|
||||
Margin="0,10,0,0"
|
||||
@@ -108,7 +110,7 @@
|
||||
Height="34">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<fi:SymbolIcon Symbol="FolderOpen" IconVariant="Regular" FontSize="16"/>
|
||||
<TextBlock Text="Open Logs"/>
|
||||
<TextBlock Text="{x:Static res:Strings.Error_ButtonOpenLogs}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
@@ -117,7 +119,7 @@
|
||||
Height="34">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<fi:SymbolIcon Symbol="Copy" IconVariant="Regular" FontSize="16"/>
|
||||
<TextBlock Text="Copy"/>
|
||||
<TextBlock Text="{x:Static res:Strings.Error_ButtonCopy}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
@@ -126,7 +128,7 @@
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Button x:Name="SecondaryActionButton"
|
||||
Content="Wait"
|
||||
Content="{x:Static res:Strings.Error_ButtonWait}"
|
||||
MinWidth="96"
|
||||
Height="34"
|
||||
IsVisible="False" />
|
||||
@@ -136,13 +138,13 @@
|
||||
Height="34">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<fi:SymbolIcon Symbol="Dismiss" IconVariant="Regular" FontSize="16"/>
|
||||
<TextBlock Text="Exit"/>
|
||||
<TextBlock Text="{x:Static res:Strings.Error_ButtonExit}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button x:Name="PrimaryActionButton"
|
||||
Classes="accent"
|
||||
Content="Retry"
|
||||
Content="{x:Static res:Strings.Error_ButtonRetry}"
|
||||
MinWidth="112"
|
||||
Height="34" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -5,6 +5,7 @@ using Avalonia.Input.Platform;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using LanMountainDesktop.Launcher.Resources;
|
||||
using LanMountainDesktop.Launcher.Services;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Views;
|
||||
@@ -36,7 +37,7 @@ public partial class ErrorWindow : Window
|
||||
public void SetErrorMessage(string message)
|
||||
{
|
||||
var normalizedMessage = string.IsNullOrWhiteSpace(message)
|
||||
? "LanMountain Desktop did not reach the expected startup state."
|
||||
? Strings.Error_MessageNotReached
|
||||
: message.Trim();
|
||||
var firstLine = normalizedMessage
|
||||
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
|
||||
@@ -58,16 +59,16 @@ public partial class ErrorWindow : Window
|
||||
_isDebugMode = isDebugMode;
|
||||
if (isDebugMode && this.FindControl<TextBlock>("TitleText") is { } titleText)
|
||||
{
|
||||
titleText.Text = "[Debug] Launcher error";
|
||||
titleText.Text = Strings.Error_DebugTitle;
|
||||
}
|
||||
}
|
||||
|
||||
public void ConfigureForHostNotFound()
|
||||
{
|
||||
ApplyActionLayout(
|
||||
title: "Launcher could not find the desktop executable",
|
||||
suggestion: "Pick another executable in debug mode, inspect logs, or retry after fixing the deployment path.",
|
||||
primaryLabel: "Retry",
|
||||
title: Strings.Error_HostNotFoundTitle,
|
||||
suggestion: Strings.Error_HostNotFoundMessage,
|
||||
primaryLabel: Strings.Error_ButtonRetry,
|
||||
primaryAction: ErrorWindowResult.Retry,
|
||||
secondaryLabel: null,
|
||||
secondaryAction: null);
|
||||
@@ -76,25 +77,27 @@ public partial class ErrorWindow : Window
|
||||
public void ConfigureForGenericFailure(bool allowRetry)
|
||||
{
|
||||
ApplyActionLayout(
|
||||
title: "Launcher could not confirm startup",
|
||||
title: Strings.Error_TitleCannotConfirm,
|
||||
suggestion: allowRetry
|
||||
? "Inspect logs, then retry once the previous startup attempt has fully finished."
|
||||
: "Inspect logs or exit. Launcher will avoid creating another desktop process while the old one is still running.",
|
||||
primaryLabel: allowRetry ? "Retry" : "Activate",
|
||||
? Strings.Error_GenericRetryMessage
|
||||
: Strings.Error_GenericNoRetryMessage,
|
||||
primaryLabel: allowRetry ? Strings.Error_ButtonRetry : Strings.Error_ButtonActivate,
|
||||
primaryAction: allowRetry ? ErrorWindowResult.Retry : ErrorWindowResult.ActivateExisting,
|
||||
secondaryLabel: allowRetry ? null : "Wait",
|
||||
secondaryLabel: allowRetry ? null : Strings.Error_ButtonWait,
|
||||
secondaryAction: allowRetry ? null : ErrorWindowResult.ContinueWaiting);
|
||||
}
|
||||
|
||||
public void ConfigureForRunningHostFailure(int? hostPid)
|
||||
{
|
||||
var pidHint = hostPid is > 0 ? $" Current host PID: {hostPid}." : string.Empty;
|
||||
var suggestion = hostPid is > 0
|
||||
? string.Format(Strings.Error_PendingMessageWithPid, hostPid)
|
||||
: Strings.Error_PendingMessage;
|
||||
ApplyActionLayout(
|
||||
title: "Startup is still pending",
|
||||
suggestion: $"The desktop process is still running, so Launcher will not start a second instance.{pidHint}",
|
||||
primaryLabel: "Activate",
|
||||
title: Strings.Error_PendingTitle,
|
||||
suggestion: suggestion,
|
||||
primaryLabel: Strings.Error_ButtonActivate,
|
||||
primaryAction: ErrorWindowResult.ActivateExisting,
|
||||
secondaryLabel: "Wait",
|
||||
secondaryLabel: Strings.Error_ButtonWait,
|
||||
secondaryAction: ErrorWindowResult.ContinueWaiting);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
|
||||
xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="600"
|
||||
d:DesignHeight="500"
|
||||
x:Class="LanMountainDesktop.Launcher.Views.LoadingDetailsWindow"
|
||||
Title="LanMountain Desktop - Loading Details"
|
||||
x:CompileBindings="False"
|
||||
Title="{x:Static res:Strings.Loading_Title}"
|
||||
Width="600"
|
||||
Height="500"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
@@ -23,12 +25,12 @@
|
||||
Padding="20,16">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="Starting LanMountain Desktop"
|
||||
<TextBlock Text="{x:Static res:Strings.Loading_StartingDesktop}"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
|
||||
<TextBlock x:Name="SubtitleText"
|
||||
Text="Initializing..."
|
||||
Text="{x:Static res:Strings.Loading_StatusInitializing}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
|
||||
</StackPanel>
|
||||
@@ -86,14 +88,14 @@
|
||||
|
||||
<TextBlock x:Name="CurrentItemName"
|
||||
Grid.Row="0" Grid.Column="1"
|
||||
Text="Initializing..."
|
||||
Text="{x:Static res:Strings.Loading_StatusInitializing}"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
|
||||
|
||||
<TextBlock x:Name="CurrentItemDescription"
|
||||
Grid.Row="1" Grid.Column="1"
|
||||
Text="Preparing components"
|
||||
Text="{x:Static res:Strings.Loading_StatusPreparing}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Margin="0,4,0,0"/>
|
||||
@@ -115,7 +117,7 @@
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Grid Grid.Row="0" Margin="12,8" ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="Loading Items"
|
||||
Text="{x:Static res:Strings.Loading_LoadingItems}"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"/>
|
||||
@@ -126,7 +128,7 @@
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Margin="0,0,4,0"/>
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="Done"
|
||||
Text="{x:Static res:Strings.Loading_Done}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"/>
|
||||
</Grid>
|
||||
@@ -199,7 +201,7 @@
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="ErrorText"
|
||||
Grid.Column="1"
|
||||
Text="An error occurred while loading."
|
||||
Text="{x:Static res:Strings.Loading_ErrorOccurred}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
|
||||
TextWrapping="Wrap"/>
|
||||
@@ -218,12 +220,12 @@
|
||||
VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
||||
<Button x:Name="DetailsButton"
|
||||
Content="Details"
|
||||
Content="{x:Static res:Strings.Loading_ButtonDetails}"
|
||||
Width="90"
|
||||
Height="32"
|
||||
FontSize="13"/>
|
||||
<Button x:Name="CancelButton"
|
||||
Content="Cancel"
|
||||
Content="{x:Static res:Strings.Loading_ButtonCancel}"
|
||||
Width="90"
|
||||
Height="32"
|
||||
FontSize="13"/>
|
||||
|
||||
@@ -3,6 +3,7 @@ using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Launcher.Resources;
|
||||
using LanMountainDesktop.Launcher.Services;
|
||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -57,7 +58,7 @@ public partial class LoadingDetailsWindow : Window
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鏇存柊鍔犺浇鐘舵€? /// </summary>
|
||||
/// 更新加载状<EFBFBD>? /// </summary>
|
||||
public void UpdateLoadingState(LoadingStateMessage state)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
@@ -120,7 +121,7 @@ public partial class LoadingDetailsWindow : Window
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鏇存柊褰撳墠娲诲姩椤? /// </summary>
|
||||
/// 更新当前活动<EFBFBD>? /// </summary>
|
||||
private void UpdateCurrentItem(LoadingStateMessage state)
|
||||
{
|
||||
var currentItem = state.ActiveItems.FirstOrDefault();
|
||||
@@ -181,7 +182,7 @@ public partial class LoadingDetailsWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
// 鎸夌姸鎬佹帓搴忥細杩涜<EFBFBD>涓?-> 绛夊緟涓?-> 宸插畬鎴?-> 澶辫触
|
||||
// 按状态排序:进行<EFBFBD>?-> 等待<E7AD89>?-> 已完<E5B7B2>?-> 失败
|
||||
var sortedItems = _items.OrderBy(i => GetStatePriority(i.State)).ToList();
|
||||
_items.Clear();
|
||||
foreach (var item in sortedItems)
|
||||
@@ -234,20 +235,20 @@ public partial class LoadingDetailsWindow : Window
|
||||
/// </summary>
|
||||
private static string GetStageDescription(StartupStage stage) => stage switch
|
||||
{
|
||||
StartupStage.Initializing => "正在初始化系统...",
|
||||
StartupStage.LoadingSettings => "正在加载设置...",
|
||||
StartupStage.LoadingPlugins => "正在加载插件...",
|
||||
StartupStage.InitializingUI => "正在初始化界面...",
|
||||
StartupStage.ShellInitialized => "桌面外壳已初始化",
|
||||
StartupStage.DesktopVisible => "桌面已经可见",
|
||||
StartupStage.ActivationRedirected => "已激活现有实例",
|
||||
StartupStage.ActivationFailed => "现有实例激活失败",
|
||||
StartupStage.Ready => "加载完成",
|
||||
_ => "正在加载..."
|
||||
StartupStage.Initializing => "正在初始化系统...",
|
||||
StartupStage.LoadingSettings => "正在加载设置...",
|
||||
StartupStage.LoadingPlugins => "正在加载插件...",
|
||||
StartupStage.InitializingUI => "正在初始化界面...",
|
||||
StartupStage.ShellInitialized => "桌面程序已初始化",
|
||||
StartupStage.DesktopVisible => "桌面已经可见",
|
||||
StartupStage.ActivationRedirected => "已激活到现有实例",
|
||||
StartupStage.ActivationFailed => "激活现有实例失败",
|
||||
StartupStage.Ready => Strings.Loading_StageReady,
|
||||
_ => "正在加载..."
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 鑾峰彇椤规弿杩? /// </summary>
|
||||
/// 获取项描<EFBFBD>? /// </summary>
|
||||
private static string GetItemDescription(LoadingItem item)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.Description))
|
||||
@@ -255,17 +256,17 @@ public partial class LoadingDetailsWindow : Window
|
||||
|
||||
return item.Type switch
|
||||
{
|
||||
LoadingItemType.Plugin => "姝e湪鍔犺浇鎻掍欢...",
|
||||
LoadingItemType.Component => "姝e湪鍔犺浇缁勪欢...",
|
||||
LoadingItemType.Resource => "姝e湪鍔犺浇璧勬簮...",
|
||||
LoadingItemType.Data => "姝e湪鍔犺浇鏁版嵁...",
|
||||
LoadingItemType.Network => "姝e湪涓嬭浇...",
|
||||
_ => "姝e湪澶勭悊..."
|
||||
LoadingItemType.Plugin => Strings.Loading_ItemPlugin,
|
||||
LoadingItemType.Component => Strings.Loading_ItemComponent,
|
||||
LoadingItemType.Resource => Strings.Loading_ItemResource,
|
||||
LoadingItemType.Data => Strings.Loading_ItemData,
|
||||
LoadingItemType.Network => Strings.Loading_ItemDownload,
|
||||
_ => Strings.Loading_ItemProcess
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鑾峰彇椤瑰浘鏍? /// </summary>
|
||||
/// 获取项图<EFBFBD>? /// </summary>
|
||||
private static string GetItemIcon(LoadingItemType type) => type switch
|
||||
{
|
||||
LoadingItemType.Plugin => "\uE768",
|
||||
@@ -294,7 +295,7 @@ public partial class LoadingDetailsWindow : Window
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鍔犺浇椤硅<EFBFBD>鍥炬ā鍨?/// </summary>
|
||||
/// 加载项视图模<EFBFBD>?/// </summary>
|
||||
public class LoadingItemViewModel : INotifyPropertyChanged
|
||||
{
|
||||
public string Id { get; }
|
||||
@@ -306,7 +307,7 @@ public class LoadingItemViewModel : INotifyPropertyChanged
|
||||
|
||||
public string StatusIcon => GetStatusIcon(State);
|
||||
public IBrush StatusColor => GetStatusColor(State);
|
||||
public string ProgressText => State == LoadingState.Completed ? "瀹屾垚" : $"{ProgressPercent}%";
|
||||
public string ProgressText => State == LoadingState.Completed ? Strings.Loading_ItemComplete : $"{ProgressPercent}%";
|
||||
public string TypeLabel => GetTypeLabel(Type);
|
||||
public IBrush TypeBackground => GetTypeBackground(Type);
|
||||
public IBrush TypeForeground => GetTypeForeground(Type);
|
||||
@@ -359,14 +360,14 @@ public class LoadingItemViewModel : INotifyPropertyChanged
|
||||
|
||||
private static string GetTypeLabel(LoadingItemType type) => type switch
|
||||
{
|
||||
LoadingItemType.Plugin => "鎻掍欢",
|
||||
LoadingItemType.Component => "缁勪欢",
|
||||
LoadingItemType.Resource => "璧勬簮",
|
||||
LoadingItemType.Data => "鏁版嵁",
|
||||
LoadingItemType.Network => "缃戠粶",
|
||||
LoadingItemType.Settings => "璁剧疆",
|
||||
LoadingItemType.System => "绯荤粺",
|
||||
_ => "鍏朵粬"
|
||||
LoadingItemType.Plugin => Strings.Loading_TypePlugin,
|
||||
LoadingItemType.Component => Strings.Loading_TypeComponent,
|
||||
LoadingItemType.Resource => Strings.Loading_TypeResource,
|
||||
LoadingItemType.Data => Strings.Loading_TypeData,
|
||||
LoadingItemType.Network => Strings.Loading_TypeNetwork,
|
||||
LoadingItemType.Settings => Strings.Loading_TypeSettings,
|
||||
LoadingItemType.System => Strings.Loading_TypeSystem,
|
||||
_ => Strings.Loading_TypeOther
|
||||
};
|
||||
|
||||
private static IBrush GetTypeBackground(LoadingItemType type) => type switch
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
|
||||
xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="520"
|
||||
d:DesignHeight="360"
|
||||
x:Class="LanMountainDesktop.Launcher.Views.MigrationPromptWindow"
|
||||
x:DataType="views:MigrationPromptWindow"
|
||||
Title="阑山桌面 - 版本迁移"
|
||||
x:CompileBindings="False"
|
||||
Title="{x:Static res:Strings.Migration_Title}"
|
||||
Width="520"
|
||||
Height="360"
|
||||
CanResize="False"
|
||||
@@ -51,7 +53,7 @@
|
||||
|
||||
<!-- 说明文字 -->
|
||||
<TextBlock x:Name="DescriptionText"
|
||||
Text="检测到您的系统中安装了旧版本的阑山桌面(0.8.4)。新版本采用了全新的架构,建议卸载旧版本以获得更好的体验。"
|
||||
Text="{x:Static res:Strings.Migration_DetectedDesc}"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
@@ -65,7 +67,7 @@
|
||||
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="Auto,*">
|
||||
<!-- 版本号 -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||
Text="版本:"
|
||||
Text="{x:Static res:Strings.Migration_Version}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"/>
|
||||
<TextBlock x:Name="VersionText"
|
||||
@@ -77,7 +79,7 @@
|
||||
|
||||
<!-- 安装路径 -->
|
||||
<TextBlock Grid.Row="1" Grid.Column="0"
|
||||
Text="位置:"
|
||||
Text="{x:Static res:Strings.Migration_Location}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
|
||||
Margin="0,4,0,0"/>
|
||||
@@ -91,13 +93,13 @@
|
||||
|
||||
<!-- 安装类型 -->
|
||||
<TextBlock Grid.Row="2" Grid.Column="0"
|
||||
Text="类型:"
|
||||
Text="{x:Static res:Strings.Migration_Type}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
|
||||
Margin="0,4,0,0"/>
|
||||
<TextBlock x:Name="TypeText"
|
||||
Grid.Row="2" Grid.Column="1"
|
||||
Text="安装版"
|
||||
Text="{x:Static res:Strings.Migration_Installed}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Margin="8,4,0,0"/>
|
||||
@@ -105,7 +107,7 @@
|
||||
</Border>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<TextBlock Text="卸载旧版本不会影响新版本的使用,您的个人数据将保留。"
|
||||
<TextBlock Text="{x:Static res:Strings.Migration_UninstallNote}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
@@ -121,7 +123,7 @@
|
||||
<!-- 左侧:查看位置按钮 -->
|
||||
<Button x:Name="ShowLocationButton"
|
||||
Grid.Column="0"
|
||||
Content="查看位置"
|
||||
Content="{x:Static res:Strings.Migration_ButtonViewLocation}"
|
||||
Width="100"
|
||||
Height="32"
|
||||
FontSize="13"
|
||||
@@ -137,7 +139,7 @@
|
||||
Height="32"
|
||||
FontSize="13"/>
|
||||
<Button x:Name="UninstallButton"
|
||||
Content="卸载旧版本"
|
||||
Content="{x:Static res:Strings.Migration_ButtonUninstall}"
|
||||
Width="100"
|
||||
Height="32"
|
||||
FontSize="13"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using LanMountainDesktop.Launcher.Resources;
|
||||
using LanMountainDesktop.Launcher.Services;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Views;
|
||||
@@ -46,15 +47,15 @@ public partial class MigrationPromptWindow : Window
|
||||
{
|
||||
typeText.Text = info.InstallType switch
|
||||
{
|
||||
LegacyInstallType.Registry => "安装版",
|
||||
LegacyInstallType.Portable => "便携版",
|
||||
_ => "未知"
|
||||
LegacyInstallType.Registry => Strings.Migration_Installed,
|
||||
LegacyInstallType.Portable => Strings.Migration_Portable,
|
||||
_ => Strings.Migration_Unknown
|
||||
};
|
||||
}
|
||||
|
||||
if (descriptionText != null)
|
||||
{
|
||||
descriptionText.Text = $"检测到您的系统中安装了旧版本的阑山桌面({info.Version})。新版本采用了全新的架构,建议卸载旧版本以获得更好的体验。";
|
||||
descriptionText.Text = string.Format(Strings.Migration_DetectedDescFormat, info.Version);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
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"
|
||||
x:Class="LanMountainDesktop.Launcher.Views.MultiInstancePromptWindow"
|
||||
x:DataType="views:MultiInstancePromptWindow"
|
||||
Title="LanMountain Desktop"
|
||||
x:CompileBindings="False"
|
||||
Title="{x:Static res:Strings.MultiInstance_Title}"
|
||||
Width="620"
|
||||
Height="360"
|
||||
MinWidth="560"
|
||||
@@ -44,13 +46,13 @@
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
Spacing="8">
|
||||
<TextBlock Text="LanMountain Desktop is already running"
|
||||
<TextBlock Text="{x:Static res:Strings.MultiInstance_AlreadyRunning}"
|
||||
FontSize="22"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock x:Name="MessageText"
|
||||
Text="Launcher found an existing desktop instance and did not start another process."
|
||||
Text="{x:Static res:Strings.MultiInstance_AlreadyRunningMessage}"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
@@ -64,8 +66,8 @@
|
||||
IsOpen="True"
|
||||
IsClosable="False"
|
||||
Severity="Informational"
|
||||
Title="Repeated launch"
|
||||
Message="Your current setting is to show this prompt without opening the desktop automatically.">
|
||||
Title="{x:Static res:Strings.MultiInstance_RepeatedLaunchTitle}"
|
||||
Message="{x:Static res:Strings.MultiInstance_RepeatedLaunchMessage}">
|
||||
<ui:FAInfoBar.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰊈"
|
||||
FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
@@ -79,7 +81,7 @@
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Text="No second Host process was created." />
|
||||
Text="{x:Static res:Strings.MultiInstance_NoSecondProcess}" />
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="1"
|
||||
@@ -93,7 +95,7 @@
|
||||
HorizontalAlignment="Left">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<fi:SymbolIcon Symbol="Copy" IconVariant="Regular" FontSize="16"/>
|
||||
<TextBlock Text="Copy"/>
|
||||
<TextBlock Text="{x:Static res:Strings.MultiInstance_ButtonCopy}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
@@ -105,7 +107,7 @@
|
||||
Height="34">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<fi:SymbolIcon Symbol="Dismiss" IconVariant="Regular" FontSize="16"/>
|
||||
<TextBlock Text="Close"/>
|
||||
<TextBlock Text="{x:Static res:Strings.MultiInstance_ButtonClose}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="OpenDesktopButton"
|
||||
@@ -114,7 +116,7 @@
|
||||
Height="34">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<fi:SymbolIcon Symbol="ArrowRight" IconVariant="Regular" FontSize="16"/>
|
||||
<TextBlock Text="Open desktop"/>
|
||||
<TextBlock Text="{x:Static res:Strings.MultiInstance_ButtonOpenDesktop}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
@@ -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<MultiInstancePromptResult> _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<TextBlock>("DetailsText") is { } detailsText)
|
||||
{
|
||||
|
||||
@@ -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 @@
|
||||
<Button.RenderTransform>
|
||||
<ScaleTransform ScaleX="0.1" ScaleY="0.1" />
|
||||
</Button.RenderTransform>
|
||||
<TextBlock Text="开始使用"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_ButtonGetStarted}"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold" />
|
||||
</Button>
|
||||
@@ -173,11 +175,11 @@
|
||||
<!-- 步骤 2: 主题选择页面 -->
|
||||
<Grid x:Name="ThemeStep" Margin="48" RowDefinitions="Auto,*,Auto" IsVisible="False">
|
||||
<StackPanel Grid.Row="0" Spacing="8" Margin="0,0,0,24">
|
||||
<TextBlock Text="个性化你的桌面"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_AppearanceTitle}"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="选择你喜欢的主题样式,可随时在设置中更改"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_AppearanceDesc}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
@@ -189,7 +191,7 @@
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
|
||||
Padding="16">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="外观模式"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_AppearanceMode}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
@@ -215,7 +217,7 @@
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Text="浅色模式"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_LightMode}"
|
||||
FontSize="13"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
@@ -247,7 +249,7 @@
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Text="深色模式"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_DarkMode}"
|
||||
FontSize="13"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
@@ -265,7 +267,7 @@
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
|
||||
Padding="16">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="主题色"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_ThemeColor}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
@@ -335,11 +337,11 @@
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
|
||||
Padding="16">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="莫奈取色来源"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_MonetSource}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="从壁纸自动提取主题色,让界面与桌面完美融合"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_MonetDesc}"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
|
||||
@@ -359,11 +361,11 @@
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" Spacing="4">
|
||||
<TextBlock Text="从桌面壁纸取色"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_MonetFromWallpaper}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="自动分析当前壁纸颜色生成主题"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_MonetFromWallpaperDesc}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
@@ -384,11 +386,11 @@
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" Spacing="4">
|
||||
<TextBlock Text="自定义图片取色"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_MonetFromCustomImage}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="选择一张图片作为取色来源"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_MonetFromCustomImageDesc}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
@@ -409,11 +411,11 @@
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" Spacing="4">
|
||||
<TextBlock Text="不使用莫奈取色"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_MonetDisabled}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="使用固定的预设主题色"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_MonetDisabledDesc}"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
@@ -431,10 +433,10 @@
|
||||
Spacing="12"
|
||||
Margin="0,24,0,0">
|
||||
<Button x:Name="ThemeBackButton"
|
||||
Content="返回"
|
||||
Content="{x:Static res:Strings.Oobe_ButtonBack}"
|
||||
Theme="{DynamicResource ButtonTheme}" />
|
||||
<Button x:Name="ThemeNextButton"
|
||||
Content="下一步"
|
||||
Content="{x:Static res:Strings.Oobe_ButtonNext}"
|
||||
Theme="{DynamicResource AccentButtonTheme}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
@@ -442,11 +444,11 @@
|
||||
<!-- 步骤 3: 数据位置选择页面 -->
|
||||
<Grid x:Name="DataLocationStep" Margin="48" RowDefinitions="Auto,*,Auto" IsVisible="False">
|
||||
<StackPanel Grid.Row="0" Spacing="8" Margin="0,0,0,32">
|
||||
<TextBlock Text="选择数据保存位置"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_DataLocationTitle}"
|
||||
FontSize="28"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="决定将应用数据保存在哪里,可随时在设置中更改"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_DataLocationDesc}"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
@@ -462,12 +464,12 @@
|
||||
<fi:SymbolIcon Symbol="ShieldError"
|
||||
FontSize="20"
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
|
||||
<TextBlock Text="无法保存到应用目录"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_NotWritable}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="当前安装目录需要管理员权限才能写入,数据将自动保存到系统用户目录。"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_NotWritableDesc}"
|
||||
FontSize="13"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
|
||||
@@ -494,12 +496,12 @@
|
||||
<fi:SymbolIcon Symbol="Folder"
|
||||
FontSize="24"
|
||||
Foreground="{DynamicResource AccentFillColorDefaultBrush}" />
|
||||
<TextBlock Text="保存在系统用户目录(推荐)"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_SystemProfile}"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="数据与当前 Windows 用户绑定,重装应用或更新后数据不会丢失。适合大多数用户。"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_SystemProfileDesc}"
|
||||
FontSize="13"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -537,12 +539,12 @@
|
||||
<fi:SymbolIcon Symbol="Save"
|
||||
FontSize="24"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
<TextBlock Text="保存在应用安装目录(便携模式)"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_Portable}"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="便于携带,可随应用文件夹整体移动到其他电脑。适合在多台电脑间使用或需要便携运行的场景。"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_PortableDesc}"
|
||||
FontSize="13"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -584,12 +586,12 @@
|
||||
Spacing="12"
|
||||
Margin="0,32,0,0">
|
||||
<Button x:Name="DataLocationBackButton"
|
||||
Content="返回"
|
||||
Content="{x:Static res:Strings.Oobe_ButtonBack}"
|
||||
Theme="{DynamicResource ButtonTheme}"
|
||||
Width="100"
|
||||
Height="36" />
|
||||
<Button x:Name="DataLocationNextButton"
|
||||
Content="下一步"
|
||||
Content="{x:Static res:Strings.Oobe_ButtonNext}"
|
||||
Theme="{DynamicResource AccentButtonTheme}"
|
||||
Width="100"
|
||||
Height="36" />
|
||||
@@ -599,11 +601,11 @@
|
||||
<!-- 步骤 4: 启动与展示(紧接数据保存位置) -->
|
||||
<Grid x:Name="StartupPresentationStep" Margin="48" RowDefinitions="Auto,*,Auto" IsVisible="False">
|
||||
<StackPanel Grid.Row="0" Spacing="8" Margin="0,0,0,16">
|
||||
<TextBlock Text="启动与展示"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_StartupTitle}"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="这些选项可随时在桌面应用的「设置」中更改。主窗口滑动入场仅在 Windows 上可用。"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_StartupDesc}"
|
||||
FontSize="13"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -616,11 +618,11 @@
|
||||
Padding="16">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="在任务栏显示主桌面窗口"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_ShowInTaskbar}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="开启后最小化时可在任务栏保留条目;关闭则更多依赖托盘图标。"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_ShowInTaskbarDesc}"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -638,11 +640,11 @@
|
||||
<StackPanel Spacing="12">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="以滑动方式显示主窗口"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_SlideTransition}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="自屏幕边缘滑入;与「淡入」二选一。"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_SlideTransitionDesc}"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -653,11 +655,11 @@
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="启动时使用淡入过渡"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_FadeTransition}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="在未启用滑动入场时建议使用。"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_FadeTransitionDesc}"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -678,12 +680,12 @@
|
||||
<fi:SymbolIcon Symbol="SlideLayout"
|
||||
FontSize="20"
|
||||
Foreground="{DynamicResource AccentFillColorDefaultBrush}" />
|
||||
<TextBlock Text="融合桌面与弹入手势"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_FusedDesktop}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="同时启用融合桌面与三指滑动手势,以便使用边缘弹入与相关实验特性(与设置中开发者选项一致)。"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_FusedDesktopDesc}"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -699,12 +701,12 @@
|
||||
Padding="16">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="登录 Windows 时自动启动阑山桌面"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_AutoStart}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock x:Name="OobeAutoStartDescriptionText"
|
||||
Text="通过当前用户的启动项注册本启动器(与安装程序可选任务使用同一注册表项)。"
|
||||
Text="{x:Static res:Strings.Oobe_AutoStartDesc}"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -723,10 +725,10 @@
|
||||
Spacing="12"
|
||||
Margin="0,24,0,0">
|
||||
<Button x:Name="StartupPresentationBackButton"
|
||||
Content="返回"
|
||||
Content="{x:Static res:Strings.Oobe_ButtonBack}"
|
||||
Theme="{DynamicResource ButtonTheme}" />
|
||||
<Button x:Name="StartupPresentationNextButton"
|
||||
Content="下一步"
|
||||
Content="{x:Static res:Strings.Oobe_ButtonNext}"
|
||||
Theme="{DynamicResource AccentButtonTheme}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
@@ -734,11 +736,11 @@
|
||||
<!-- 步骤 5: 信息与隐私页面 -->
|
||||
<Grid x:Name="PrivacyStep" Margin="48" RowDefinitions="Auto,*,Auto" IsVisible="False">
|
||||
<StackPanel Grid.Row="0" Spacing="8" Margin="0,0,0,24">
|
||||
<TextBlock Text="信息与隐私"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_PrivacyTitle}"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="选择是否参与遥测计划,查看隐私政策"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_PrivacyDesc}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
@@ -750,11 +752,11 @@
|
||||
Padding="16">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="发送匿名崩溃报告"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_CrashReports}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="帮助改进应用稳定性,不包含个人身份信息"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_CrashReportsDesc}"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -773,11 +775,11 @@
|
||||
Padding="16">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="发送匿名使用统计"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_UsageStats}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="帮助了解功能使用情况,优化产品体验"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_UsageStatsDesc}"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -795,11 +797,11 @@
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
|
||||
Padding="16">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="隐私追踪 ID"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_PrivacyTrackingId}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="此 ID 用于匿名标识您的设备,不包含任何个人信息"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_PrivacyTrackingIdDesc}"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -823,12 +825,12 @@
|
||||
<CheckBox x:Name="PrivacyAgreementCheckBox"
|
||||
VerticalAlignment="Center" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
|
||||
<TextBlock Text="同意"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_Agree}"
|
||||
FontSize="13"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<Button x:Name="ViewPrivacyPolicyButton"
|
||||
Content="《阑山桌面遥测隐私数据收集协议》"
|
||||
Content="{x:Static res:Strings.Oobe_PrivacyPolicyLink}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
@@ -844,7 +846,7 @@
|
||||
</StackPanel>
|
||||
|
||||
<!-- 提示文本 -->
|
||||
<TextBlock Text="您必须阅读并同意隐私协议后,才能开启遥测功能。遥测数据仅用于改进应用稳定性和优化产品体验,不包含任何个人身份信息。"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_PrivacyAgreementNote}"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -858,10 +860,10 @@
|
||||
Spacing="12"
|
||||
Margin="0,24,0,0">
|
||||
<Button x:Name="PrivacyBackButton"
|
||||
Content="返回"
|
||||
Content="{x:Static res:Strings.Oobe_ButtonBack}"
|
||||
Theme="{DynamicResource ButtonTheme}" />
|
||||
<Button x:Name="PrivacyNextButton"
|
||||
Content="下一步"
|
||||
Content="{x:Static res:Strings.Oobe_ButtonNext}"
|
||||
Theme="{DynamicResource AccentButtonTheme}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
@@ -887,12 +889,12 @@
|
||||
</Border>
|
||||
|
||||
<StackPanel Spacing="12" HorizontalAlignment="Center">
|
||||
<TextBlock Text="欢迎使用阑山桌面"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_CompleteTitle}"
|
||||
FontSize="32"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock Text="你的桌面,不止一面"
|
||||
<TextBlock Text="{x:Static res:Strings.Oobe_CompleteSubtitle}"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
@@ -7,6 +7,7 @@ using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Launcher.Models;
|
||||
using LanMountainDesktop.Launcher.Resources;
|
||||
using LanMountainDesktop.Launcher.Services;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Views;
|
||||
@@ -272,7 +273,7 @@ public partial class OobeWindow : Window
|
||||
if (typingTextBlock == null || cursorBorder == null) return;
|
||||
|
||||
// 打字机效果:阑山桌面 LanMountain Desktop(在同一行)
|
||||
var fullText = "阑山桌面 LanMountain Desktop";
|
||||
var fullText = Strings.Oobe_TypingAppName;
|
||||
for (int i = 0; i <= fullText.Length; i++)
|
||||
{
|
||||
typingTextBlock.Text = fullText.Substring(0, i);
|
||||
@@ -375,7 +376,7 @@ public partial class OobeWindow : Window
|
||||
}
|
||||
|
||||
// 打字机效果:下一代
|
||||
var nextGenText = "下一代";
|
||||
var nextGenText = Strings.Oobe_TypingNextGen;
|
||||
for (int i = 0; i <= nextGenText.Length; i++)
|
||||
{
|
||||
nextGenTextBlock.Text = nextGenText.Substring(0, i);
|
||||
@@ -392,7 +393,7 @@ public partial class OobeWindow : Window
|
||||
}
|
||||
|
||||
// 打字机效果:互动信息看板
|
||||
var dashboardText = "互动信息看板";
|
||||
var dashboardText = Strings.Oobe_TypingDashboard;
|
||||
for (int i = 0; i <= dashboardText.Length; i++)
|
||||
{
|
||||
dashboardTextBlock.Text = dashboardText.Substring(0, i);
|
||||
@@ -531,8 +532,8 @@ public partial class OobeWindow : Window
|
||||
if (this.FindControl<TextBlock>("OobeAutoStartDescriptionText") is { } autoStartDesc)
|
||||
{
|
||||
autoStartDesc.Text = OperatingSystem.IsWindows()
|
||||
? "通过当前用户的启动项注册本启动器(与安装程序可选任务使用同一注册表项)。"
|
||||
: "当前平台仅保存偏好;是否随系统自启动请使用系统提供的应用自启动设置。";
|
||||
? Strings.Oobe_AutoStartDesc
|
||||
: Strings.Oobe_AutoStartDescNonWindows;
|
||||
}
|
||||
|
||||
_suppressOobeStartupTransitionHandlers = true;
|
||||
@@ -770,7 +771,7 @@ public partial class OobeWindow : Window
|
||||
}
|
||||
if (this.FindControl<TextBlock>("MigrationInfoText") is { } migrationText)
|
||||
{
|
||||
migrationText.Text = "检测到现有数据,选择便携模式时将自动迁移。";
|
||||
migrationText.Text = Strings.Oobe_MigrationDetected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:md="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
|
||||
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
|
||||
xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources"
|
||||
mc:Ignorable="d"
|
||||
x:Class="LanMountainDesktop.Launcher.Views.PrivacyPolicyWindow"
|
||||
x:DataType="views:PrivacyPolicyViewModel"
|
||||
Title="阑山桌面遥测隐私数据收集协议"
|
||||
x:CompileBindings="False"
|
||||
Title="{x:Static res:Strings.Privacy_Title}"
|
||||
Width="800"
|
||||
Height="600"
|
||||
MinWidth="600"
|
||||
@@ -25,11 +27,11 @@
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="24,16">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="阑山桌面遥测隐私数据收集协议"
|
||||
<TextBlock Text="{x:Static res:Strings.Privacy_Header}"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="请仔细阅读以下协议内容,了解我们如何收集、使用和保护您的数据"
|
||||
<TextBlock Text="{x:Static res:Strings.Privacy_Description}"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
@@ -54,7 +56,7 @@
|
||||
HorizontalAlignment="Right"
|
||||
Spacing="12">
|
||||
<Button x:Name="CloseButton"
|
||||
Content="关闭"
|
||||
Content="{x:Static res:Strings.Privacy_ButtonClose}"
|
||||
Theme="{DynamicResource AccentButtonTheme}"
|
||||
Width="100" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
|
||||
xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources"
|
||||
mc:Ignorable="d"
|
||||
x:Class="LanMountainDesktop.Launcher.Views.SplashWindow"
|
||||
x:DataType="views:SplashWindow"
|
||||
Title="LanMountain Desktop"
|
||||
x:CompileBindings="False"
|
||||
Title="{x:Static res:Strings.Splash_Title}"
|
||||
Width="480"
|
||||
Height="320"
|
||||
CanResize="False"
|
||||
@@ -66,7 +68,7 @@
|
||||
FontSize="11"
|
||||
Foreground="#B9C0CC"
|
||||
HorizontalAlignment="Right"
|
||||
Text="Initializing..." />
|
||||
Text="{x:Static res:Strings.Splash_StatusInitializing}" />
|
||||
</Grid>
|
||||
|
||||
<ProgressBar x:Name="ProgressIndicator"
|
||||
|
||||
@@ -6,6 +6,7 @@ using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Launcher.Resources;
|
||||
using LanMountainDesktop.Launcher.Services;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Views;
|
||||
@@ -184,7 +185,7 @@ public partial class SplashWindow : Window, ISplashStageReporter
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateStatus("[Debug Mode] Splash Preview");
|
||||
UpdateStatus(Strings.Splash_DebugPreview);
|
||||
}
|
||||
|
||||
private void OnVersionTextClick(object? sender, PointerPressedEventArgs e)
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
|
||||
xmlns:res="clr-namespace:LanMountainDesktop.Launcher.Resources"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="480"
|
||||
d:DesignHeight="320"
|
||||
x:Class="LanMountainDesktop.Launcher.Views.UpdateWindow"
|
||||
x:DataType="views:UpdateWindow"
|
||||
Title="阑山桌面 - 更新"
|
||||
x:CompileBindings="False"
|
||||
Title="{x:Static res:Strings.Update_Title}"
|
||||
Width="480"
|
||||
Height="320"
|
||||
CanResize="False"
|
||||
@@ -26,7 +28,7 @@
|
||||
<Grid VerticalAlignment="Top" Margin="24,24,24,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center" Spacing="8">
|
||||
<TextBlock x:Name="TitleText"
|
||||
Text="阑山桌面"
|
||||
Text="{x:Static res:Strings.Update_AppName}"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
@@ -34,7 +36,7 @@
|
||||
CornerRadius="4"
|
||||
Padding="6,2"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="Update"
|
||||
<TextBlock Text="{x:Static res:Strings.Update_StatusUpdate}"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}" />
|
||||
@@ -80,7 +82,7 @@
|
||||
Opacity="0.8"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Bottom"
|
||||
Text="正在更新,请稍候..." />
|
||||
Text="{x:Static res:Strings.Update_StatusUpdating}" />
|
||||
|
||||
<!-- 右下角:百分比 -->
|
||||
<TextBlock x:Name="PercentText"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Launcher.Resources;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Views;
|
||||
|
||||
@@ -87,12 +88,12 @@ public partial class UpdateWindow : Window
|
||||
|
||||
if (success)
|
||||
{
|
||||
statusText.Text = "更新完成";
|
||||
statusText.Text = Strings.Update_Complete;
|
||||
}
|
||||
else
|
||||
{
|
||||
titleText.Text = "更新失败";
|
||||
statusText.Text = errorMessage ?? "更新过程中发生错误";
|
||||
titleText.Text = Strings.Update_Failed;
|
||||
statusText.Text = errorMessage ?? Strings.Update_FailedMessage;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -115,8 +116,8 @@ public partial class UpdateWindow : Window
|
||||
|
||||
if (isDebugMode)
|
||||
{
|
||||
titleText.Text = "[调试模式] 更新页面";
|
||||
statusText.Text = "预览更新进度界面";
|
||||
titleText.Text = Strings.Update_DebugTitle;
|
||||
statusText.Text = Strings.Update_DebugMessage;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,6 +79,22 @@ public sealed class AirAppLauncherServiceTests
|
||||
key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildSingleInstanceKey_UsesGlobalClockSuiteForWorldClock()
|
||||
{
|
||||
var analogKey = AirAppLauncherService.BuildSingleInstanceKey(
|
||||
AirAppLauncherService.WorldClockAppId,
|
||||
BuiltInComponentIds.DesktopClock,
|
||||
"analog-placement");
|
||||
var worldKey = AirAppLauncherService.BuildSingleInstanceKey(
|
||||
AirAppLauncherService.WorldClockAppId,
|
||||
BuiltInComponentIds.DesktopWorldClock,
|
||||
"world-placement");
|
||||
|
||||
Assert.Equal("world-clock:clock-suite:global", analogKey);
|
||||
Assert.Equal(analogKey, worldKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateBrokerStartInfo_UsesAirAppBrokerCommandAndRequesterPid()
|
||||
{
|
||||
|
||||
176
LanMountainDesktop.Tests/ClockAirAppMvpTests.cs
Normal file
176
LanMountainDesktop.Tests/ClockAirAppMvpTests.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using LanMountainDesktop.Services.ClockAirApp;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class ClockAirAppMvpTests
|
||||
{
|
||||
[Fact]
|
||||
public void SettingsSnapshot_DefaultsMatchClockSuiteMvp()
|
||||
{
|
||||
var snapshot = ClockAirAppSettingsSnapshot.Normalize(null);
|
||||
|
||||
Assert.Equal(ClockAirAppTimeFormatMode.System, snapshot.TimeFormatMode);
|
||||
Assert.True(snapshot.ShowSeconds);
|
||||
Assert.Equal(ClockAirAppTabIds.Last, snapshot.StartupTab);
|
||||
Assert.Equal(ClockAirAppTabIds.WorldClock, snapshot.LastSelectedTab);
|
||||
Assert.True(snapshot.ActivateOnTimerFinished);
|
||||
Assert.Equal(4, snapshot.WorldClockTimeZoneIds.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingsStore_LoadsDefaultsWhenJsonIsBroken()
|
||||
{
|
||||
var directory = CreateTempDirectory();
|
||||
var path = Path.Combine(directory, "settings.json");
|
||||
File.WriteAllText(path, "{ broken json");
|
||||
|
||||
var store = new ClockAirAppSettingsStore(path);
|
||||
var snapshot = store.Load();
|
||||
|
||||
Assert.Equal(ClockAirAppTimeFormatMode.System, snapshot.TimeFormatMode);
|
||||
Assert.Equal(4, snapshot.WorldClockTimeZoneIds.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingsStore_SavesAndLoadsIndependentClockSettings()
|
||||
{
|
||||
var directory = CreateTempDirectory();
|
||||
var path = Path.Combine(directory, "settings.json");
|
||||
var store = new ClockAirAppSettingsStore(path);
|
||||
|
||||
store.Save(new ClockAirAppSettingsSnapshot
|
||||
{
|
||||
TimeFormatMode = ClockAirAppTimeFormatMode.TwelveHour,
|
||||
ShowSeconds = false,
|
||||
StartupTab = ClockAirAppTabIds.Timer,
|
||||
LastSelectedTab = ClockAirAppTabIds.Stopwatch,
|
||||
ActivateOnTimerFinished = false,
|
||||
WorldClockTimeZoneIds = ["UTC"]
|
||||
});
|
||||
|
||||
var loaded = store.Load();
|
||||
Assert.Equal(ClockAirAppTimeFormatMode.TwelveHour, loaded.TimeFormatMode);
|
||||
Assert.False(loaded.ShowSeconds);
|
||||
Assert.Equal(ClockAirAppTabIds.Timer, loaded.StartupTab);
|
||||
Assert.Equal(ClockAirAppTabIds.Stopwatch, loaded.LastSelectedTab);
|
||||
Assert.False(loaded.ActivateOnTimerFinished);
|
||||
Assert.Single(loaded.WorldClockTimeZoneIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TimeFormatter_FormatsTimeAndOffsets()
|
||||
{
|
||||
var time = new DateTime(2026, 5, 18, 21, 7, 9);
|
||||
var settings = new ClockAirAppSettingsSnapshot
|
||||
{
|
||||
TimeFormatMode = ClockAirAppTimeFormatMode.TwentyFourHour,
|
||||
ShowSeconds = true
|
||||
};
|
||||
|
||||
Assert.Equal("21:07:09", ClockAirAppTimeFormatter.FormatTime(time, settings, CultureInfo.GetCultureInfo("en-US")));
|
||||
Assert.Equal("UTC+08:30", ClockAirAppTimeFormatter.FormatUtcOffset(TimeSpan.FromMinutes(510)));
|
||||
Assert.Equal("UTC-05:00", ClockAirAppTimeFormatter.FormatUtcOffset(TimeSpan.FromHours(-5)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StopwatchState_StartPauseLapAndReset()
|
||||
{
|
||||
var state = new ClockAirAppStopwatchState();
|
||||
var start = DateTimeOffset.Parse("2026-05-18T12:00:00Z", CultureInfo.InvariantCulture);
|
||||
|
||||
state.StartOrResume(start);
|
||||
Assert.True(state.IsRunning);
|
||||
Assert.Equal(TimeSpan.FromSeconds(5), state.GetElapsed(start.AddSeconds(5)));
|
||||
|
||||
var lap = state.AddLap(start.AddSeconds(6));
|
||||
Assert.Equal(TimeSpan.FromSeconds(6), lap);
|
||||
Assert.Single(state.Laps);
|
||||
|
||||
state.Pause(start.AddSeconds(8));
|
||||
Assert.False(state.IsRunning);
|
||||
Assert.Equal(TimeSpan.FromSeconds(8), state.GetElapsed(start.AddSeconds(20)));
|
||||
|
||||
state.Reset();
|
||||
Assert.Equal(TimeSpan.Zero, state.GetElapsed(start.AddSeconds(30)));
|
||||
Assert.Empty(state.Laps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TimerState_StartPauseAndComplete()
|
||||
{
|
||||
var state = new ClockAirAppTimerState();
|
||||
var start = DateTimeOffset.Parse("2026-05-18T12:00:00Z", CultureInfo.InvariantCulture);
|
||||
|
||||
state.SetDuration(TimeSpan.FromSeconds(10));
|
||||
state.StartOrResume(start);
|
||||
Assert.True(state.IsRunning);
|
||||
Assert.Equal(TimeSpan.FromSeconds(6), state.GetRemaining(start.AddSeconds(4)));
|
||||
|
||||
state.Pause(start.AddSeconds(4));
|
||||
Assert.False(state.IsRunning);
|
||||
Assert.Equal(TimeSpan.FromSeconds(6), state.GetRemaining(start.AddSeconds(20)));
|
||||
|
||||
state.StartOrResume(start.AddSeconds(20));
|
||||
Assert.False(state.Update(start.AddSeconds(25)));
|
||||
Assert.True(state.Update(start.AddSeconds(26)));
|
||||
Assert.True(state.IsCompleted);
|
||||
Assert.Equal(TimeSpan.Zero, state.GetRemaining(start.AddSeconds(26)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LocalizationFiles_ContainClockAirAppKeys()
|
||||
{
|
||||
var requiredKeys = new[]
|
||||
{
|
||||
"clockairapp.title",
|
||||
"clockairapp.tab.world",
|
||||
"clockairapp.tab.stopwatch",
|
||||
"clockairapp.tab.timer",
|
||||
"clockairapp.tab.settings",
|
||||
"clockairapp.settings.time_format.24h"
|
||||
};
|
||||
|
||||
foreach (var language in new[] { "zh-CN", "en-US", "ja-JP", "ko-KR" })
|
||||
{
|
||||
var json = ReadRepositoryFile("LanMountainDesktop", "Localization", $"{language}.json");
|
||||
var table = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
Assert.NotNull(table);
|
||||
foreach (var key in requiredKeys)
|
||||
{
|
||||
Assert.True(table!.ContainsKey(key), $"{language} is missing {key}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateTempDirectory()
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(), "LanMountainDesktop.Tests", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
private static string ReadRepositoryFile(params string[] segments)
|
||||
{
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (directory is not null)
|
||||
{
|
||||
var candidate = Path.Combine(new[] { directory.FullName }.Concat(segments).ToArray());
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return File.ReadAllText(candidate);
|
||||
}
|
||||
|
||||
if (File.Exists(Path.Combine(directory.FullName, "LanMountainDesktop.slnx")))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
throw new FileNotFoundException($"Could not locate repository file '{Path.Combine(segments)}'.");
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,32 @@ public sealed class LauncherAirAppLifecycleServiceTests
|
||||
Assert.Equal(first.Instance!.InstanceKey, second.Instance!.InstanceKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenAsync_ReusesGlobalClockSuiteAcrossClockComponents()
|
||||
{
|
||||
var starter = new TestAirAppProcessStarter(Process.GetCurrentProcess());
|
||||
var service = new LauncherAirAppLifecycleService(starter);
|
||||
|
||||
var first = await service.OpenAsync(new AirAppOpenRequest(
|
||||
"world-clock",
|
||||
BuiltInComponentIds.DesktopClock,
|
||||
"analog-placement",
|
||||
Environment.ProcessId));
|
||||
var second = await service.OpenAsync(new AirAppOpenRequest(
|
||||
"world-clock",
|
||||
BuiltInComponentIds.DesktopWorldClock,
|
||||
"world-placement",
|
||||
Environment.ProcessId));
|
||||
|
||||
Assert.True(first.Accepted);
|
||||
Assert.True(second.Accepted);
|
||||
Assert.Equal("started", first.Code);
|
||||
Assert.Equal("activated_existing", second.Code);
|
||||
Assert.Equal("world-clock:clock-suite:global", first.Instance!.InstanceKey);
|
||||
Assert.Equal(first.Instance.InstanceKey, second.Instance!.InstanceKey);
|
||||
Assert.Equal(1, starter.StartCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenAsync_PrunesExitedRegisteredInstanceBeforeRestart()
|
||||
{
|
||||
|
||||
@@ -16,6 +16,22 @@ public sealed class WindowLayerIsolationTests
|
||||
Assert.DoesNotContain("Topmost=true", source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AirAppWindow_UsesFluentAvaloniaChromeInsteadOfHandRolledTitleBar()
|
||||
{
|
||||
var xaml = ReadRepositoryFile("LanMountainDesktop.AirAppHost", "AirAppWindow.axaml");
|
||||
var source = ReadRepositoryFile("LanMountainDesktop.AirAppHost", "AirAppWindow.axaml.cs");
|
||||
|
||||
Assert.Contains("<faWindowing:FAAppWindow", xaml);
|
||||
Assert.Contains(": FAAppWindow", source);
|
||||
Assert.Contains("ShowAsDialog", source);
|
||||
Assert.Contains("TitleBar.ExtendsContentIntoTitleBar", source);
|
||||
Assert.DoesNotContain("OnTitleBarPointerPressed", source);
|
||||
Assert.DoesNotContain("BeginMoveDrag", source);
|
||||
Assert.DoesNotContain("OnCloseClick", source);
|
||||
Assert.DoesNotContain("PointerPressed=\"OnTitleBarPointerPressed\"", xaml);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AirAppWindowDescriptor_DefinesSupportedChromeModes()
|
||||
{
|
||||
@@ -36,12 +52,29 @@ public sealed class WindowLayerIsolationTests
|
||||
|
||||
Assert.Contains("AirAppLaunchOptions.WorldClockAppId", source);
|
||||
Assert.Contains("AirAppWindowChromeMode.Standard", source);
|
||||
Assert.Contains("width: 360", source);
|
||||
Assert.Contains("height: 220", source);
|
||||
Assert.Contains("width: 780", source);
|
||||
Assert.Contains("height: 560", source);
|
||||
Assert.Contains("minWidth: 680", source);
|
||||
Assert.Contains("minHeight: 480", source);
|
||||
Assert.Contains("canResize: true", source);
|
||||
Assert.Contains("showAsDialog: false", source);
|
||||
Assert.Contains("AirAppLaunchOptions.WhiteboardAppId", source);
|
||||
Assert.Contains("AirAppWindowChromeMode.FullScreen", source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AirAppWindow_LoadsClockSuiteForWorldClockApp()
|
||||
{
|
||||
var source = ReadRepositoryFile("LanMountainDesktop.AirAppHost", "AirAppWindow.axaml.cs");
|
||||
var viewXaml = ReadRepositoryFile("LanMountainDesktop.AirAppHost", "ClockAirAppView.axaml");
|
||||
var projectFile = ReadRepositoryFile("LanMountainDesktop.AirAppHost", "LanMountainDesktop.AirAppHost.csproj");
|
||||
|
||||
Assert.Contains("new ClockAirAppView(_options)", source);
|
||||
Assert.Contains("clock-suite:global", source);
|
||||
Assert.Contains("ClockAirAppView", viewXaml);
|
||||
Assert.Contains("Localization\\*.json", projectFile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DesktopComponentHost_DoesNotInterceptLivePointerInputForAirApps()
|
||||
{
|
||||
|
||||
@@ -1097,6 +1097,40 @@
|
||||
"clock.settings.second_mode_label": "Second Hand",
|
||||
"clock.second_mode.tick": "Tick",
|
||||
"clock.second_mode.sweep": "Sweep",
|
||||
"clockairapp.title": "Clock",
|
||||
"clockairapp.subtitle": "World clock, stopwatch and timer",
|
||||
"clockairapp.tab.world": "World",
|
||||
"clockairapp.tab.stopwatch": "Stopwatch",
|
||||
"clockairapp.tab.timer": "Timer",
|
||||
"clockairapp.tab.settings": "Settings",
|
||||
"clockairapp.world.local": "Local time",
|
||||
"clockairapp.world.add": "Add",
|
||||
"clockairapp.world.search": "Search city or time zone",
|
||||
"clockairapp.world.count": "{0} cities",
|
||||
"clockairapp.action.start": "Start",
|
||||
"clockairapp.action.pause": "Pause",
|
||||
"clockairapp.action.reset": "Reset",
|
||||
"clockairapp.action.remove": "Remove",
|
||||
"clockairapp.action.move_up": "Move up",
|
||||
"clockairapp.action.move_down": "Move down",
|
||||
"clockairapp.stopwatch.hint": "Lap timing stays in this window session.",
|
||||
"clockairapp.stopwatch.lap": "Lap",
|
||||
"clockairapp.stopwatch.lap_format": "Lap {0} {1}",
|
||||
"clockairapp.timer.hint": "Choose a preset or enter custom minutes.",
|
||||
"clockairapp.timer.apply": "Apply",
|
||||
"clockairapp.timer.minutes": "Minutes",
|
||||
"clockairapp.timer.finished": "Timer finished",
|
||||
"clockairapp.timer.duration_status": "Duration {0}",
|
||||
"clockairapp.timer.invalid": "Enter a valid minute value.",
|
||||
"clockairapp.settings.title": "Clock settings",
|
||||
"clockairapp.settings.time_format": "Time format",
|
||||
"clockairapp.settings.startup_tab": "Startup page",
|
||||
"clockairapp.settings.show_seconds": "Show seconds",
|
||||
"clockairapp.settings.activate_timer": "Activate window when timer finishes",
|
||||
"clockairapp.settings.time_format.system": "Follow system",
|
||||
"clockairapp.settings.time_format.24h": "24-hour",
|
||||
"clockairapp.settings.time_format.12h": "12-hour",
|
||||
"clockairapp.settings.startup.last": "Last used",
|
||||
"poetry.widget.loading_content": "Loading poetry...",
|
||||
"poetry.widget.loading_author": "Loading...",
|
||||
"poetry.widget.fetch_failed": "Poetry fetch failed",
|
||||
|
||||
@@ -811,6 +811,40 @@
|
||||
"desktop_clock.settings.second_mode_label": "秒針",
|
||||
"clock.second_mode.tick": "ティック",
|
||||
"clock.second_mode.sweep": "スイープ",
|
||||
"clockairapp.title": "時計",
|
||||
"clockairapp.subtitle": "世界時計、ストップウォッチ、タイマー",
|
||||
"clockairapp.tab.world": "世界時計",
|
||||
"clockairapp.tab.stopwatch": "ストップウォッチ",
|
||||
"clockairapp.tab.timer": "タイマー",
|
||||
"clockairapp.tab.settings": "設定",
|
||||
"clockairapp.world.local": "ローカル時刻",
|
||||
"clockairapp.world.add": "追加",
|
||||
"clockairapp.world.search": "都市またはタイムゾーンを検索",
|
||||
"clockairapp.world.count": "{0} 都市",
|
||||
"clockairapp.action.start": "開始",
|
||||
"clockairapp.action.pause": "一時停止",
|
||||
"clockairapp.action.reset": "リセット",
|
||||
"clockairapp.action.remove": "削除",
|
||||
"clockairapp.action.move_up": "上へ",
|
||||
"clockairapp.action.move_down": "下へ",
|
||||
"clockairapp.stopwatch.hint": "ラップは現在のウィンドウセッション内に保持されます。",
|
||||
"clockairapp.stopwatch.lap": "ラップ",
|
||||
"clockairapp.stopwatch.lap_format": "ラップ {0} {1}",
|
||||
"clockairapp.timer.hint": "プリセットを選ぶか、分数を入力します。",
|
||||
"clockairapp.timer.apply": "適用",
|
||||
"clockairapp.timer.minutes": "分",
|
||||
"clockairapp.timer.finished": "タイマー終了",
|
||||
"clockairapp.timer.duration_status": "時間 {0}",
|
||||
"clockairapp.timer.invalid": "有効な分数を入力してください。",
|
||||
"clockairapp.settings.title": "時計設定",
|
||||
"clockairapp.settings.time_format": "時刻形式",
|
||||
"clockairapp.settings.startup_tab": "起動ページ",
|
||||
"clockairapp.settings.show_seconds": "秒を表示",
|
||||
"clockairapp.settings.activate_timer": "タイマー終了時にウィンドウを前面へ",
|
||||
"clockairapp.settings.time_format.system": "システムに従う",
|
||||
"clockairapp.settings.time_format.24h": "24時間",
|
||||
"clockairapp.settings.time_format.12h": "12時間",
|
||||
"clockairapp.settings.startup.last": "前回使用",
|
||||
"poetry.widget.loading_content": "詩を読み込み中...",
|
||||
"poetry.widget.loading_author": "読み込み中...",
|
||||
"poetry.widget.fetch_failed": "詩の取得に失敗しました",
|
||||
|
||||
@@ -857,6 +857,40 @@
|
||||
"desktop_clock.settings.second_mode_label": "초침 방식",
|
||||
"clock.second_mode.tick": "똑딱이",
|
||||
"clock.second_mode.sweep": "스윕",
|
||||
"clockairapp.title": "시계",
|
||||
"clockairapp.subtitle": "세계 시계, 스톱워치, 타이머",
|
||||
"clockairapp.tab.world": "세계 시계",
|
||||
"clockairapp.tab.stopwatch": "스톱워치",
|
||||
"clockairapp.tab.timer": "타이머",
|
||||
"clockairapp.tab.settings": "설정",
|
||||
"clockairapp.world.local": "현지 시간",
|
||||
"clockairapp.world.add": "추가",
|
||||
"clockairapp.world.search": "도시 또는 시간대 검색",
|
||||
"clockairapp.world.count": "{0}개 도시",
|
||||
"clockairapp.action.start": "시작",
|
||||
"clockairapp.action.pause": "일시정지",
|
||||
"clockairapp.action.reset": "초기화",
|
||||
"clockairapp.action.remove": "삭제",
|
||||
"clockairapp.action.move_up": "위로",
|
||||
"clockairapp.action.move_down": "아래로",
|
||||
"clockairapp.stopwatch.hint": "랩 기록은 현재 창 세션에만 유지됩니다.",
|
||||
"clockairapp.stopwatch.lap": "랩",
|
||||
"clockairapp.stopwatch.lap_format": "랩 {0} {1}",
|
||||
"clockairapp.timer.hint": "프리셋을 선택하거나 사용자 지정 분을 입력하세요.",
|
||||
"clockairapp.timer.apply": "적용",
|
||||
"clockairapp.timer.minutes": "분",
|
||||
"clockairapp.timer.finished": "타이머 종료",
|
||||
"clockairapp.timer.duration_status": "시간 {0}",
|
||||
"clockairapp.timer.invalid": "올바른 분 값을 입력하세요.",
|
||||
"clockairapp.settings.title": "시계 설정",
|
||||
"clockairapp.settings.time_format": "시간 형식",
|
||||
"clockairapp.settings.startup_tab": "시작 페이지",
|
||||
"clockairapp.settings.show_seconds": "초 표시",
|
||||
"clockairapp.settings.activate_timer": "타이머 종료 시 창 활성화",
|
||||
"clockairapp.settings.time_format.system": "시스템 설정 따르기",
|
||||
"clockairapp.settings.time_format.24h": "24시간",
|
||||
"clockairapp.settings.time_format.12h": "12시간",
|
||||
"clockairapp.settings.startup.last": "마지막 사용",
|
||||
"poetry.widget.loading_content": "시 불러오는 중",
|
||||
"poetry.widget.loading_author": "로딩 중",
|
||||
"poetry.widget.fetch_failed": "시 가져오기 실패",
|
||||
|
||||
@@ -1027,6 +1027,40 @@
|
||||
"clock.settings.second_mode_label": "秒针方式",
|
||||
"clock.second_mode.tick": "跳针",
|
||||
"clock.second_mode.sweep": "扫针",
|
||||
"clockairapp.title": "时钟",
|
||||
"clockairapp.subtitle": "世界时钟、秒表和计时器",
|
||||
"clockairapp.tab.world": "世界时钟",
|
||||
"clockairapp.tab.stopwatch": "秒表",
|
||||
"clockairapp.tab.timer": "计时器",
|
||||
"clockairapp.tab.settings": "设置",
|
||||
"clockairapp.world.local": "本地时间",
|
||||
"clockairapp.world.add": "添加",
|
||||
"clockairapp.world.search": "搜索城市或时区",
|
||||
"clockairapp.world.count": "{0} 个城市",
|
||||
"clockairapp.action.start": "开始",
|
||||
"clockairapp.action.pause": "暂停",
|
||||
"clockairapp.action.reset": "重置",
|
||||
"clockairapp.action.remove": "移除",
|
||||
"clockairapp.action.move_up": "上移",
|
||||
"clockairapp.action.move_down": "下移",
|
||||
"clockairapp.stopwatch.hint": "计次记录仅保留在当前窗口会话中。",
|
||||
"clockairapp.stopwatch.lap": "计次",
|
||||
"clockairapp.stopwatch.lap_format": "计次 {0} {1}",
|
||||
"clockairapp.timer.hint": "选择预设时长,或输入自定义分钟数。",
|
||||
"clockairapp.timer.apply": "应用",
|
||||
"clockairapp.timer.minutes": "分钟",
|
||||
"clockairapp.timer.finished": "计时结束",
|
||||
"clockairapp.timer.duration_status": "时长 {0}",
|
||||
"clockairapp.timer.invalid": "请输入有效的分钟数。",
|
||||
"clockairapp.settings.title": "时钟设置",
|
||||
"clockairapp.settings.time_format": "时间格式",
|
||||
"clockairapp.settings.startup_tab": "启动页面",
|
||||
"clockairapp.settings.show_seconds": "显示秒数",
|
||||
"clockairapp.settings.activate_timer": "计时结束时激活窗口",
|
||||
"clockairapp.settings.time_format.system": "跟随系统",
|
||||
"clockairapp.settings.time_format.24h": "24 小时制",
|
||||
"clockairapp.settings.time_format.12h": "12 小时制",
|
||||
"clockairapp.settings.startup.last": "上次使用",
|
||||
"poetry.widget.loading_content": "正在加载诗词",
|
||||
"poetry.widget.loading_author": "加载中",
|
||||
"poetry.widget.fetch_failed": "诗词获取失败",
|
||||
|
||||
@@ -64,6 +64,11 @@ internal sealed class AirAppLauncherService : IAirAppLauncherService
|
||||
internal static string BuildSingleInstanceKey(string appId, string? sourceComponentId, string? sourcePlacementId)
|
||||
{
|
||||
var normalizedAppId = string.IsNullOrWhiteSpace(appId) ? "unknown" : appId.Trim();
|
||||
if (string.Equals(normalizedAppId, WorldClockAppId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return $"{normalizedAppId}:clock-suite:global";
|
||||
}
|
||||
|
||||
var normalizedComponentId = string.IsNullOrWhiteSpace(sourceComponentId) ? "none" : sourceComponentId.Trim();
|
||||
var normalizedPlacementId = string.IsNullOrWhiteSpace(sourcePlacementId) ? "none" : sourcePlacementId.Trim();
|
||||
return $"{normalizedAppId}:{normalizedComponentId}:{normalizedPlacementId}";
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Services.ClockAirApp;
|
||||
|
||||
public sealed class ClockAirAppSettingsSnapshot
|
||||
{
|
||||
public string TimeFormatMode { get; set; } = ClockAirAppTimeFormatMode.System;
|
||||
|
||||
public bool ShowSeconds { get; set; } = true;
|
||||
|
||||
public string StartupTab { get; set; } = ClockAirAppTabIds.Last;
|
||||
|
||||
public string LastSelectedTab { get; set; } = ClockAirAppTabIds.WorldClock;
|
||||
|
||||
public bool ActivateOnTimerFinished { get; set; } = true;
|
||||
|
||||
public List<string> WorldClockTimeZoneIds { get; set; } =
|
||||
[
|
||||
"China Standard Time",
|
||||
"GMT Standard Time",
|
||||
"AUS Eastern Standard Time",
|
||||
"Eastern Standard Time"
|
||||
];
|
||||
|
||||
public ClockAirAppSettingsSnapshot Clone()
|
||||
{
|
||||
return new ClockAirAppSettingsSnapshot
|
||||
{
|
||||
TimeFormatMode = ClockAirAppTimeFormatMode.Normalize(TimeFormatMode),
|
||||
ShowSeconds = ShowSeconds,
|
||||
StartupTab = ClockAirAppTabIds.Normalize(StartupTab, ClockAirAppTabIds.Last),
|
||||
LastSelectedTab = ClockAirAppTabIds.Normalize(LastSelectedTab),
|
||||
ActivateOnTimerFinished = ActivateOnTimerFinished,
|
||||
WorldClockTimeZoneIds = WorldClockTimeZoneIds is { Count: > 0 }
|
||||
? new List<string>(WorldClockTimeZoneIds.Where(static id => !string.IsNullOrWhiteSpace(id)).Select(static id => id.Trim()))
|
||||
: []
|
||||
};
|
||||
}
|
||||
|
||||
public static ClockAirAppSettingsSnapshot Normalize(ClockAirAppSettingsSnapshot? snapshot)
|
||||
{
|
||||
var normalized = (snapshot ?? new ClockAirAppSettingsSnapshot()).Clone();
|
||||
if (normalized.WorldClockTimeZoneIds.Count == 0)
|
||||
{
|
||||
normalized.WorldClockTimeZoneIds =
|
||||
[
|
||||
"China Standard Time",
|
||||
"GMT Standard Time",
|
||||
"AUS Eastern Standard Time",
|
||||
"Eastern Standard Time"
|
||||
];
|
||||
}
|
||||
|
||||
normalized.WorldClockTimeZoneIds = normalized.WorldClockTimeZoneIds
|
||||
.Select(static id => WorldClockTimeZoneCatalog.ResolveTimeZoneOrLocal(id).Id)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Services.ClockAirApp;
|
||||
|
||||
public sealed class ClockAirAppSettingsStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
private readonly string _settingsPath;
|
||||
|
||||
public ClockAirAppSettingsStore()
|
||||
: this(Path.Combine(AppDataPathProvider.GetDataRoot(), "AirApps", "Clock", "settings.json"))
|
||||
{
|
||||
}
|
||||
|
||||
public ClockAirAppSettingsStore(string settingsPath)
|
||||
{
|
||||
_settingsPath = settingsPath;
|
||||
}
|
||||
|
||||
public string SettingsPath => _settingsPath;
|
||||
|
||||
public ClockAirAppSettingsSnapshot Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_settingsPath))
|
||||
{
|
||||
return ClockAirAppSettingsSnapshot.Normalize(null);
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(_settingsPath);
|
||||
var snapshot = JsonSerializer.Deserialize<ClockAirAppSettingsSnapshot>(json, SerializerOptions);
|
||||
return ClockAirAppSettingsSnapshot.Normalize(snapshot);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("ClockAirApp", $"Failed to load clock Air APP settings from '{_settingsPath}'.", ex);
|
||||
return ClockAirAppSettingsSnapshot.Normalize(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(ClockAirAppSettingsSnapshot snapshot)
|
||||
{
|
||||
var normalized = ClockAirAppSettingsSnapshot.Normalize(snapshot);
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_settingsPath);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
File.WriteAllText(_settingsPath, JsonSerializer.Serialize(normalized, SerializerOptions));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("ClockAirApp", $"Failed to save clock Air APP settings to '{_settingsPath}'.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LanMountainDesktop.Services.ClockAirApp;
|
||||
|
||||
public sealed class ClockAirAppStopwatchState
|
||||
{
|
||||
private readonly List<TimeSpan> _laps = [];
|
||||
private TimeSpan _elapsedBeforeRun = TimeSpan.Zero;
|
||||
private DateTimeOffset? _startedAt;
|
||||
|
||||
public bool IsRunning => _startedAt.HasValue;
|
||||
|
||||
public IReadOnlyList<TimeSpan> Laps => _laps;
|
||||
|
||||
public TimeSpan GetElapsed(DateTimeOffset now)
|
||||
{
|
||||
return _startedAt.HasValue
|
||||
? _elapsedBeforeRun + (now - _startedAt.Value)
|
||||
: _elapsedBeforeRun;
|
||||
}
|
||||
|
||||
public void StartOrResume(DateTimeOffset now)
|
||||
{
|
||||
if (_startedAt.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_startedAt = now;
|
||||
}
|
||||
|
||||
public void Pause(DateTimeOffset now)
|
||||
{
|
||||
if (!_startedAt.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_elapsedBeforeRun = GetElapsed(now);
|
||||
_startedAt = null;
|
||||
}
|
||||
|
||||
public TimeSpan AddLap(DateTimeOffset now)
|
||||
{
|
||||
var elapsed = GetElapsed(now);
|
||||
_laps.Insert(0, elapsed);
|
||||
if (_laps.Count > 50)
|
||||
{
|
||||
_laps.RemoveRange(50, _laps.Count - 50);
|
||||
}
|
||||
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_elapsedBeforeRun = TimeSpan.Zero;
|
||||
_startedAt = null;
|
||||
_laps.Clear();
|
||||
}
|
||||
}
|
||||
23
LanMountainDesktop/Services/ClockAirApp/ClockAirAppTabIds.cs
Normal file
23
LanMountainDesktop/Services/ClockAirApp/ClockAirAppTabIds.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace LanMountainDesktop.Services.ClockAirApp;
|
||||
|
||||
public static class ClockAirAppTabIds
|
||||
{
|
||||
public const string Last = "last";
|
||||
public const string WorldClock = "world";
|
||||
public const string Stopwatch = "stopwatch";
|
||||
public const string Timer = "timer";
|
||||
public const string Settings = "settings";
|
||||
|
||||
public static string Normalize(string? value, string fallback = WorldClock)
|
||||
{
|
||||
return value?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
Last => Last,
|
||||
WorldClock => WorldClock,
|
||||
Stopwatch => Stopwatch,
|
||||
Timer => Timer,
|
||||
Settings => Settings,
|
||||
_ => fallback
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace LanMountainDesktop.Services.ClockAirApp;
|
||||
|
||||
public static class ClockAirAppTimeFormatMode
|
||||
{
|
||||
public const string System = "system";
|
||||
public const string TwentyFourHour = "24h";
|
||||
public const string TwelveHour = "12h";
|
||||
|
||||
public static string Normalize(string? value)
|
||||
{
|
||||
return value?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
TwentyFourHour => TwentyFourHour,
|
||||
TwelveHour => TwelveHour,
|
||||
_ => System
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace LanMountainDesktop.Services.ClockAirApp;
|
||||
|
||||
public static class ClockAirAppTimeFormatter
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> CityNames =
|
||||
new Dictionary<string, IReadOnlyDictionary<string, string>>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["zh-CN"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["China Standard Time"] = "北京",
|
||||
["Asia/Shanghai"] = "北京",
|
||||
["GMT Standard Time"] = "伦敦",
|
||||
["Europe/London"] = "伦敦",
|
||||
["AUS Eastern Standard Time"] = "悉尼",
|
||||
["Australia/Sydney"] = "悉尼",
|
||||
["Eastern Standard Time"] = "纽约",
|
||||
["America/New_York"] = "纽约",
|
||||
["Tokyo Standard Time"] = "东京",
|
||||
["Asia/Tokyo"] = "东京",
|
||||
["UTC"] = "UTC",
|
||||
["Etc/UTC"] = "UTC"
|
||||
},
|
||||
["en-US"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["China Standard Time"] = "Beijing",
|
||||
["Asia/Shanghai"] = "Beijing",
|
||||
["GMT Standard Time"] = "London",
|
||||
["Europe/London"] = "London",
|
||||
["AUS Eastern Standard Time"] = "Sydney",
|
||||
["Australia/Sydney"] = "Sydney",
|
||||
["Eastern Standard Time"] = "New York",
|
||||
["America/New_York"] = "New York",
|
||||
["Tokyo Standard Time"] = "Tokyo",
|
||||
["Asia/Tokyo"] = "Tokyo",
|
||||
["UTC"] = "UTC",
|
||||
["Etc/UTC"] = "UTC"
|
||||
},
|
||||
["ja-JP"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["China Standard Time"] = "北京",
|
||||
["Asia/Shanghai"] = "北京",
|
||||
["GMT Standard Time"] = "ロンドン",
|
||||
["Europe/London"] = "ロンドン",
|
||||
["AUS Eastern Standard Time"] = "シドニー",
|
||||
["Australia/Sydney"] = "シドニー",
|
||||
["Eastern Standard Time"] = "ニューヨーク",
|
||||
["America/New_York"] = "ニューヨーク",
|
||||
["Tokyo Standard Time"] = "東京",
|
||||
["Asia/Tokyo"] = "東京",
|
||||
["UTC"] = "UTC",
|
||||
["Etc/UTC"] = "UTC"
|
||||
},
|
||||
["ko-KR"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["China Standard Time"] = "베이징",
|
||||
["Asia/Shanghai"] = "베이징",
|
||||
["GMT Standard Time"] = "런던",
|
||||
["Europe/London"] = "런던",
|
||||
["AUS Eastern Standard Time"] = "시드니",
|
||||
["Australia/Sydney"] = "시드니",
|
||||
["Eastern Standard Time"] = "뉴욕",
|
||||
["America/New_York"] = "뉴욕",
|
||||
["Tokyo Standard Time"] = "도쿄",
|
||||
["Asia/Tokyo"] = "도쿄",
|
||||
["UTC"] = "UTC",
|
||||
["Etc/UTC"] = "UTC"
|
||||
}
|
||||
};
|
||||
|
||||
public static string FormatTime(DateTime time, ClockAirAppSettingsSnapshot settings, CultureInfo culture)
|
||||
{
|
||||
var use24Hour = UseTwentyFourHourClock(settings.TimeFormatMode, culture);
|
||||
var showSeconds = settings.ShowSeconds;
|
||||
var format = use24Hour
|
||||
? showSeconds ? "HH:mm:ss" : "HH:mm"
|
||||
: showSeconds ? "h:mm:ss tt" : "h:mm tt";
|
||||
return time.ToString(format, culture);
|
||||
}
|
||||
|
||||
public static string FormatDuration(TimeSpan duration, bool includeMilliseconds = false)
|
||||
{
|
||||
if (duration < TimeSpan.Zero)
|
||||
{
|
||||
duration = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
var totalHours = (int)duration.TotalHours;
|
||||
return includeMilliseconds
|
||||
? string.Create(CultureInfo.InvariantCulture, $"{totalHours:D2}:{duration.Minutes:D2}:{duration.Seconds:D2}.{duration.Milliseconds / 10:D2}")
|
||||
: string.Create(CultureInfo.InvariantCulture, $"{totalHours:D2}:{duration.Minutes:D2}:{duration.Seconds:D2}");
|
||||
}
|
||||
|
||||
public static string FormatUtcOffset(TimeSpan offset)
|
||||
{
|
||||
var sign = offset >= TimeSpan.Zero ? "+" : "-";
|
||||
var totalMinutes = Math.Abs((int)Math.Round(offset.TotalMinutes));
|
||||
var hours = totalMinutes / 60;
|
||||
var minutes = totalMinutes % 60;
|
||||
return $"UTC{sign}{hours:D2}:{minutes:D2}";
|
||||
}
|
||||
|
||||
public static string ResolveCityName(TimeZoneInfo timeZone, string languageCode)
|
||||
{
|
||||
var normalizedLanguage = NormalizeLanguage(languageCode);
|
||||
if (CityNames.TryGetValue(normalizedLanguage, out var cityNames) &&
|
||||
cityNames.TryGetValue(timeZone.Id, out var cityName))
|
||||
{
|
||||
return cityName;
|
||||
}
|
||||
|
||||
var normalized = timeZone.Id;
|
||||
var slashIndex = normalized.LastIndexOf('/');
|
||||
if (slashIndex >= 0 && slashIndex < normalized.Length - 1)
|
||||
{
|
||||
normalized = normalized[(slashIndex + 1)..];
|
||||
}
|
||||
|
||||
normalized = normalized.Replace('_', ' ').Trim();
|
||||
normalized = normalized
|
||||
.Replace("Standard Time", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("Daylight Time", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("Time", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Trim();
|
||||
|
||||
return string.IsNullOrWhiteSpace(normalized) ? timeZone.Id : normalized;
|
||||
}
|
||||
|
||||
public static bool UseTwentyFourHourClock(string? timeFormatMode, CultureInfo culture)
|
||||
{
|
||||
return ClockAirAppTimeFormatMode.Normalize(timeFormatMode) switch
|
||||
{
|
||||
ClockAirAppTimeFormatMode.TwentyFourHour => true,
|
||||
ClockAirAppTimeFormatMode.TwelveHour => false,
|
||||
_ => !culture.DateTimeFormat.ShortTimePattern.Contains('h')
|
||||
};
|
||||
}
|
||||
|
||||
private static string NormalizeLanguage(string? languageCode)
|
||||
{
|
||||
return languageCode?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"en" or "en-us" => "en-US",
|
||||
"ja" or "ja-jp" => "ja-JP",
|
||||
"ko" or "ko-kr" => "ko-KR",
|
||||
_ => "zh-CN"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
|
||||
namespace LanMountainDesktop.Services.ClockAirApp;
|
||||
|
||||
public sealed class ClockAirAppTimerState
|
||||
{
|
||||
private TimeSpan _duration = TimeSpan.FromMinutes(5);
|
||||
private TimeSpan _remainingBeforeRun = TimeSpan.FromMinutes(5);
|
||||
private DateTimeOffset? _startedAt;
|
||||
|
||||
public TimeSpan Duration => _duration;
|
||||
|
||||
public bool IsRunning => _startedAt.HasValue;
|
||||
|
||||
public bool IsCompleted { get; private set; }
|
||||
|
||||
public TimeSpan GetRemaining(DateTimeOffset now)
|
||||
{
|
||||
if (!_startedAt.HasValue)
|
||||
{
|
||||
return _remainingBeforeRun < TimeSpan.Zero ? TimeSpan.Zero : _remainingBeforeRun;
|
||||
}
|
||||
|
||||
var remaining = _remainingBeforeRun - (now - _startedAt.Value);
|
||||
return remaining < TimeSpan.Zero ? TimeSpan.Zero : remaining;
|
||||
}
|
||||
|
||||
public void SetDuration(TimeSpan duration)
|
||||
{
|
||||
if (duration <= TimeSpan.Zero)
|
||||
{
|
||||
duration = TimeSpan.FromMinutes(1);
|
||||
}
|
||||
|
||||
_duration = duration;
|
||||
Reset();
|
||||
}
|
||||
|
||||
public void StartOrResume(DateTimeOffset now)
|
||||
{
|
||||
if (_startedAt.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_remainingBeforeRun <= TimeSpan.Zero || IsCompleted)
|
||||
{
|
||||
_remainingBeforeRun = _duration;
|
||||
IsCompleted = false;
|
||||
}
|
||||
|
||||
_startedAt = now;
|
||||
}
|
||||
|
||||
public void Pause(DateTimeOffset now)
|
||||
{
|
||||
if (!_startedAt.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_remainingBeforeRun = GetRemaining(now);
|
||||
_startedAt = null;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_remainingBeforeRun = _duration;
|
||||
_startedAt = null;
|
||||
IsCompleted = false;
|
||||
}
|
||||
|
||||
public bool Update(DateTimeOffset now)
|
||||
{
|
||||
if (!_startedAt.HasValue || GetRemaining(now) > TimeSpan.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_remainingBeforeRun = TimeSpan.Zero;
|
||||
_startedAt = null;
|
||||
if (IsCompleted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IsCompleted = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -258,7 +258,7 @@ See `docs/EXTERNAL_IPC_ARCHITECTURE.md` for the detailed contract and migration
|
||||
|
||||
- `LanMountainDesktop.AirAppHost` owns Air APP window chrome through `AirAppWindowDescriptor`.
|
||||
- Supported chrome modes are `Standard`, `Borderless`, `FullScreen`, `Tool`, and reserved `BackgroundOnly`.
|
||||
- Built-in `world-clock` uses `Standard` chrome with the LanMountain custom title bar.
|
||||
- Built-in `world-clock` uses `Standard` chrome with FluentAvalonia `FAAppWindow` title-bar controls.
|
||||
- Built-in `whiteboard` uses `FullScreen` chrome and supplies its own in-app exit affordance.
|
||||
|
||||
## Launcher OOBE / Elevation Contract
|
||||
|
||||
321
docs/auto_commit_md/20260518_93758fc0.md
Normal file
321
docs/auto_commit_md/20260518_93758fc0.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# Git 提交分析报告
|
||||
|
||||
## 提交基本信息
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **提交哈希** | `93758fc08355d1f523180aa22ab8f3b40b080ed4` |
|
||||
| **作者** | lincube <lincube3@hotmail.com> |
|
||||
| **提交时间** | 2026-05-18 08:30:40 |
|
||||
| **提交分支** | - |
|
||||
| **提交类型** | Feature (feat) |
|
||||
|
||||
## 提交信息摘要
|
||||
|
||||
**feat.数字时钟,白板功能修复**
|
||||
|
||||
本次提交主要包含两个功能更新:
|
||||
1. 新增待机数字时钟组件(Standby Digital Clock)
|
||||
2. 修复白板(Whiteboard)组件的视口布局同步和墨迹颜色处理问题
|
||||
|
||||
---
|
||||
|
||||
## 变更统计
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| **修改文件数** | 28 个 |
|
||||
| **新增行数** | +1,729 行 |
|
||||
| **删除行数** | -81 行 |
|
||||
| **净增行数** | +1,648 行 |
|
||||
|
||||
### 按文件类型分布
|
||||
|
||||
- **AXAML 文件**:7 个(UI 布局文件)
|
||||
- **C# 源文件**:15 个(逻辑代码)
|
||||
- **文档文件**:3 个(规格文档)
|
||||
- **项目文件**:2 个(.csproj)
|
||||
- **测试文件**:1 个(单元测试)
|
||||
|
||||
---
|
||||
|
||||
## 详细变更分析
|
||||
|
||||
### 1. 新增功能:待机数字时钟组件
|
||||
|
||||
#### 1.1 新增文件
|
||||
|
||||
**规格文档 (.comate/specs/standby-digital-clock/)**
|
||||
- `doc.md`(291 行)- 待机数字时钟的详细规格说明
|
||||
- `summary.md`(52 行)- 功能摘要
|
||||
- `tasks.md`(25 行)- 任务清单
|
||||
|
||||
**UI 组件文件**
|
||||
- `StandbyDigitalClockWidget.axaml`(149 行)- 数字时钟 XAML 布局
|
||||
- `StandbyDigitalClockWidget.axaml.cs`(489 行)- 数字时钟逻辑实现
|
||||
|
||||
#### 1.2 核心功能特性
|
||||
|
||||
根据代码分析,待机数字时钟组件具备以下功能:
|
||||
|
||||
**时间显示**
|
||||
- 12/24 小时制切换支持
|
||||
- 多语言本地化支持(中文、英文等)
|
||||
- 自动根据系统语言设置选择文化格式
|
||||
|
||||
**夜间模式**
|
||||
- 自动检测系统主题(Dark/Light)
|
||||
- 根据背景亮度智能判断夜间模式
|
||||
- 独立的夜间模式配色方案
|
||||
|
||||
**视觉效果**
|
||||
- 渐变背景效果
|
||||
- 自适应文字颜色
|
||||
- 响应式缩放支持(0.58x - 1.95x)
|
||||
|
||||
**样式细节**
|
||||
- 半透明日期显示(#7E8593 颜色)
|
||||
- 模拟时钟元素(日/夜指示器)
|
||||
- 多时区支持(默认北京时间)
|
||||
|
||||
#### 1.3 配置与设置
|
||||
|
||||
```csharp
|
||||
// 设置加载流程
|
||||
- 语言代码规范化处理
|
||||
- 时区解析(支持 "China Standard Time" 等)
|
||||
- 颜色方案动态切换
|
||||
```
|
||||
|
||||
### 2. 功能修复:白板组件
|
||||
|
||||
#### 2.1 视口布局同步问题
|
||||
|
||||
**问题描述**
|
||||
- 白板组件在附加到视觉树或大小变化时,存在视口布局同步不及时的问题
|
||||
|
||||
**解决方案**
|
||||
|
||||
1. **新增视口大小解析记录结构**
|
||||
```csharp
|
||||
internal readonly record struct WhiteboardViewportSizeResolution(
|
||||
Size Size,
|
||||
string Source,
|
||||
bool IsFallback
|
||||
);
|
||||
```
|
||||
|
||||
2. **多层回退机制**
|
||||
- 第一层:使用 `ViewportRoot.Bounds.Size`
|
||||
- 第二层:使用 `CanvasBorder.Bounds.Size`
|
||||
- 第三层:使用基于单元格大小的计算值
|
||||
|
||||
3. **异步队列同步**
|
||||
```csharp
|
||||
private void QueueViewportLayoutSync(string reason)
|
||||
{
|
||||
Dispatcher.UIThread.Post(
|
||||
() => SynchronizeViewportLayout(reason),
|
||||
DispatcherPriority.Loaded);
|
||||
}
|
||||
```
|
||||
|
||||
4. **新增事件处理器**
|
||||
- `OnViewportRootSizeChanged`
|
||||
- `OnColorPickerPopupClosed`
|
||||
|
||||
#### 2.2 墨迹颜色透明度修复
|
||||
|
||||
**问题描述**
|
||||
- 用户选择的颜色通过颜色选择器后,透明度设置不正确
|
||||
|
||||
**修复代码**
|
||||
```csharp
|
||||
// 修复前
|
||||
InkColorPicker.Color = new Color(
|
||||
_selectedInkColor.Alpha, // 不正确的透明度来源
|
||||
_selectedInkColor.Red,
|
||||
_selectedInkColor.Green,
|
||||
_selectedInkColor.Blue);
|
||||
|
||||
// 修复后
|
||||
InkColorPicker.Color = new Color(
|
||||
byte.MaxValue, // 强制使用完全不透明
|
||||
_selectedInkColor.Red,
|
||||
_selectedInkColor.Green,
|
||||
_selectedInkColor.Blue);
|
||||
```
|
||||
|
||||
#### 2.3 墨迹输入恢复机制
|
||||
|
||||
**新增方法**
|
||||
```csharp
|
||||
private void RestoreInkInputAfterToolPopup(string reason, int attempt = 0)
|
||||
```
|
||||
|
||||
**重试策略**
|
||||
- 最多尝试 3 次
|
||||
- 使用 `DispatcherPriority.Background` 延迟执行
|
||||
- 异常处理与日志记录
|
||||
|
||||
### 3. AirApp 集成更新
|
||||
|
||||
#### 3.1 AirAppHost 项目变更
|
||||
|
||||
- `AirApp.axaml` - 更新了 AirApp 的 XAML 布局
|
||||
- `AirAppLaunchOptions.cs` - 优化了启动选项处理
|
||||
- `AirAppWindow.axaml.cs` - 新增了窗口相关功能
|
||||
- `AirAppWindowDescriptor.cs` - 更新了窗口描述符
|
||||
- `Program.cs` - 修改了程序入口逻辑
|
||||
|
||||
#### 3.2 世界时钟交互改进
|
||||
|
||||
**WorldClockWidget.axaml.cs 变更**
|
||||
```csharp
|
||||
// 新增日志记录
|
||||
AppLogger.Info(
|
||||
"AirAppLauncher",
|
||||
$"World clock component clicked. ComponentId='{_componentId}'; PlacementId='{_placementId}'.");
|
||||
|
||||
// 修改了方法签名
|
||||
AirAppLauncherServiceProvider.GetOrCreate().OpenWorldClock(
|
||||
_componentId, // 新增参数
|
||||
_placementId);
|
||||
```
|
||||
|
||||
### 4. 组件系统更新
|
||||
|
||||
#### 4.1 组件注册
|
||||
|
||||
**BuiltInComponentIds.cs**
|
||||
```csharp
|
||||
// 新增待机数字时钟组件 ID
|
||||
public const string DesktopStandbyDigitalClock = "...";
|
||||
```
|
||||
|
||||
**ComponentRegistry.cs**
|
||||
- 更新了组件注册表
|
||||
|
||||
#### 4.2 布局规则
|
||||
|
||||
**MainWindow.ComponentSystem.cs**
|
||||
```csharp
|
||||
// 新增待机数字时钟的缩放规则(与现有世界时钟一致)
|
||||
if (string.Equals(componentId, BuiltInComponentIds.DesktopStandbyDigitalClock, ...))
|
||||
{
|
||||
// 保持 2:1 宽高比
|
||||
return SnapSpanToScaleRules(span,
|
||||
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.3 删除旧代码
|
||||
|
||||
**移除的代码**
|
||||
- `TryOpenAirAppFromDesktopComponent` 方法被移除
|
||||
- 相关的桌面组件点击处理逻辑被简化
|
||||
|
||||
### 5. 测试覆盖
|
||||
|
||||
#### 5.1 新增测试文件
|
||||
|
||||
- `AirAppLauncherServiceTests.cs`(15 行)- AirApp 启动服务测试
|
||||
- `WhiteboardWidgetLayoutSyncTests.cs`(155 行)- 白板布局同步测试
|
||||
- `WindowLayerIsolationTests.cs`(69 行)- 窗口层级隔离测试
|
||||
|
||||
---
|
||||
|
||||
## 代码审查要点
|
||||
|
||||
### ✅ 优点
|
||||
|
||||
1. **良好的日志记录**
|
||||
- 使用 `AppLogger.Info` 记录关键操作
|
||||
- 包含丰富的上下文信息(ComponentId, PlacementId 等)
|
||||
|
||||
2. **完善的错误处理**
|
||||
- 白板墨迹恢复机制包含重试逻辑
|
||||
- 异常捕获和降级处理
|
||||
|
||||
3. **响应式设计**
|
||||
- 数字时钟支持多级缩放
|
||||
- 主题自适应支持
|
||||
|
||||
4. **向后兼容性**
|
||||
- 保留了现有世界时钟的布局规则
|
||||
|
||||
### ⚠️ 建议关注
|
||||
|
||||
1. **视口同步频率**
|
||||
- `DispatcherPriority.Loaded` 可能导致频繁重绘
|
||||
- 建议监控性能影响
|
||||
|
||||
2. **颜色透明度处理**
|
||||
- 修改为强制不透明可能会影响某些使用场景
|
||||
- 建议添加配置选项
|
||||
|
||||
3. **时区解析**
|
||||
- 使用 "China Standard Time" 作为默认时区
|
||||
- 考虑添加用户可配置的默认时区
|
||||
|
||||
4. **测试覆盖**
|
||||
- 新增测试文件内容需要进一步验证
|
||||
- 建议添加集成测试
|
||||
|
||||
---
|
||||
|
||||
## 文件变更清单
|
||||
|
||||
### 新增文件
|
||||
|
||||
```
|
||||
.comate/specs/standby-digital-clock/doc.md (+291 行)
|
||||
.comate/specs/standby-digital-clock/summary.md (+52 行)
|
||||
.comate/specs/standby-digital-clock/tasks.md (+25 行)
|
||||
LanMountainDesktop/Views/Components/StandbyDigitalClockWidget.axaml (+149 行)
|
||||
LanMountainDesktop/Views/Components/StandbyDigitalClockWidget.axaml.cs (+489 行)
|
||||
LanMountainDesktop.AirAppHost/LanMountainDesktop.AirAppHost.csproj (+1 行)
|
||||
```
|
||||
|
||||
### 修改文件
|
||||
|
||||
```
|
||||
LanMountainDesktop.AirAppHost/AirApp.axaml (±52 行)
|
||||
LanMountainDesktop.AirAppHost/AirAppLaunchOptions.cs (±19 行)
|
||||
LanMountainDesktop.AirAppHost/AirAppWindow.axaml.cs (+42 行)
|
||||
LanMountainDesktop.AirAppHost/AirAppWindowDescriptor.cs (±6 行)
|
||||
LanMountainDesktop.AirAppHost/Program.cs (±36 行)
|
||||
LanMountainDesktop.AirAppHost/WorldClockAirAppView.axaml (±10 行)
|
||||
LanMountainDesktop.Launcher/App.axaml.cs (±8 行)
|
||||
LanMountainDesktop.Launcher/Services/AirApp/IAirAppProcessStarter.cs (±32 行)
|
||||
LanMountainDesktop.Launcher/Tests/Services/AirApp/AirAppLauncherServiceTests.cs (+15 行)
|
||||
LanMountainDesktop/Tests/ComponentSystem/WhiteboardWidgetLayoutSyncTests.cs (+155 行)
|
||||
LanMountainDesktop/Tests/ComponentSystem/WindowLayerIsolationTests.cs (+69 行)
|
||||
LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs (+1 行)
|
||||
LanMountainDesktop/ComponentSystem/ComponentRegistry.cs (±9 行)
|
||||
LanMountainDesktop/Desktop/DesktopEditing/DesktopEditGhostView.cs (±20 行)
|
||||
LanMountainDesktop/Desktop/DesktopEditing/DesktopEditOverlayPresenter.cs (±30 行)
|
||||
LanMountainDesktop/Launcher/Services/AirApp/AirAppLauncherService.cs (±18 行)
|
||||
LanMountainDesktop/Services/AppDataPathProvider.cs (±10 行)
|
||||
LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs (±31 行)
|
||||
LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs (+4 行)
|
||||
LanMountainDesktop/Views/Components/WhiteboardWidget.axaml.cs (+202 行)
|
||||
LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs (±5 行)
|
||||
LanMountainDesktop/Views/MainWindow.ComponentSystem.cs (±29 行)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 结论
|
||||
|
||||
本次提交是一次重要的功能更新,主要包含:
|
||||
|
||||
1. **新功能**:待机数字时钟组件的完整实现
|
||||
2. **缺陷修复**:白板组件的多项交互和渲染问题
|
||||
3. **架构优化**:AirApp 集成和组件系统的改进
|
||||
4. **测试补充**:新增单元测试提高代码覆盖率
|
||||
|
||||
建议后续关注:
|
||||
- 数字时钟组件的用户界面测试
|
||||
- 白板组件在复杂场景下的性能表现
|
||||
- 时区功能在不同文化环境下的显示效果
|
||||
Reference in New Issue
Block a user