mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
372b5b7adc | ||
|
|
74703582e7 | ||
|
|
26ff11b16b | ||
|
|
b83cfb47b0 | ||
|
|
a0bb83c743 | ||
|
|
af2e7b4f2f | ||
|
|
798124e500 | ||
|
|
95ecb06668 | ||
|
|
ac7e8db516 | ||
|
|
8ded721f46 | ||
|
|
a559325f5a | ||
|
|
b60368527f | ||
|
|
c8c3f51bff | ||
|
|
685323e057 | ||
|
|
def21c79b1 | ||
|
|
c3db5af923 | ||
|
|
1a7dde34d0 | ||
|
|
73cdefe296 | ||
|
|
46a8df5900 | ||
|
|
2a1c09ae39 | ||
|
|
33baaa579d | ||
|
|
20cd6041a7 | ||
|
|
65a3cf832a | ||
|
|
5d48a03f57 | ||
|
|
ea8ce1f5ff | ||
|
|
aeae4be060 | ||
|
|
915739ff7b | ||
|
|
cb86ca10e7 | ||
|
|
b3a74aa072 | ||
|
|
b436bfa884 | ||
|
|
081abeb688 | ||
|
|
594a62132f | ||
|
|
15e589aedd | ||
|
|
ac4617f5cf | ||
|
|
0645598753 | ||
|
|
dadd132b4f | ||
|
|
298defb829 |
16
.codex/environments/environment.toml
Normal file
16
.codex/environments/environment.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
|
||||
version = 1
|
||||
name = "LanMountainDesktop"
|
||||
|
||||
[setup]
|
||||
script = ""
|
||||
|
||||
[[actions]]
|
||||
name = "运行"
|
||||
icon = "run"
|
||||
command = "dotnet run --project 'C:\\Users\\USER693091\\Documents\\GitHub\\LanMountainDesktop\\LanMountainDesktop\\LanMountainDesktop.csproj"
|
||||
|
||||
[[actions]]
|
||||
name = "构建"
|
||||
icon = "tool"
|
||||
command = "dotnet build 'C:\\Users\\USER693091\\Documents\\GitHub\\LanMountainDesktop\\LanMountainDesktop.slnx"
|
||||
27
.github/workflows/airappmarket-validate.yml
vendored
27
.github/workflows/airappmarket-validate.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: AirAppMarket Validate
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "airappmarket/**"
|
||||
- ".github/workflows/airappmarket-validate.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "airappmarket/**"
|
||||
- ".github/workflows/airappmarket-validate.yml"
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "10.0.x"
|
||||
|
||||
- name: Validate AirAppMarket index
|
||||
run: dotnet run --project airappmarket/tools/AirAppMarket.Validator -- airappmarket/index.json airappmarket/schema/airappmarket-index.schema.json
|
||||
28
.github/workflows/build.yml
vendored
28
.github/workflows/build.yml
vendored
@@ -113,3 +113,31 @@ jobs:
|
||||
path: |
|
||||
LanMountainDesktop/bin/Release/
|
||||
retention-days: 7
|
||||
|
||||
pack-plugin-packages:
|
||||
runs-on: ubuntu-latest
|
||||
name: Pack_Plugin_Packages
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||
|
||||
- name: Pack SDK and template packages
|
||||
shell: pwsh
|
||||
run: .\scripts\Pack-PluginPackages.ps1 -Configuration Release -OutputPath .\artifacts\nuget
|
||||
|
||||
- name: Upload plugin package artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: plugin-packages
|
||||
path: artifacts/nuget/*.nupkg
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
91
AGENTS.md
Normal file
91
AGENTS.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# LanMountainDesktop AI Guide
|
||||
|
||||
本文件是 AI 助手进入本仓库时的第一入口。面向 Codex、Cursor、Trae 等工具,目标是减少重复探索,快速定位权威文档、关键目录和执行约束。
|
||||
|
||||
## 1. 项目目标与仓库边界
|
||||
|
||||
- 本仓库是阑山桌面桌面宿主、宿主侧插件运行时、Plugin SDK、共享契约与基础外观/设置能力的权威来源。
|
||||
- 不要把插件市场元数据、开发者门户或官方示例插件实现当作本仓库内容维护。
|
||||
- 市场和生态材料属于兄弟仓库 `LanAirApp`。
|
||||
- 官方示例插件属于独立仓库 `LanMountainDesktop.SamplePlugin`。
|
||||
|
||||
边界详情看:
|
||||
|
||||
- `docs/ECOSYSTEM_BOUNDARIES.md`
|
||||
- `docs/ARCHITECTURE.md`
|
||||
|
||||
## 2. 关键目录地图
|
||||
|
||||
- `LanMountainDesktop/`: 主宿主应用,包含 UI、服务、组件系统、主题与插件运行时接入
|
||||
- `LanMountainDesktop/ComponentSystem/`: 内置组件定义、注册、扩展加载
|
||||
- `LanMountainDesktop/plugins/`: 宿主侧插件运行时、安装与 market 集成
|
||||
- `LanMountainDesktop/Views/` and `ViewModels/`: UI 页面、窗口与视图模型
|
||||
- `LanMountainDesktop/Services/`: 设置、遥测、启动、持久化、业务服务
|
||||
- `LanMountainDesktop.PluginSdk/`: 插件 SDK 公共接口和默认打包行为
|
||||
- `LanMountainDesktop.Shared.Contracts/`: 宿主/插件共享契约
|
||||
- `LanMountainDesktop.Tests/`: 宿主与 SDK 测试
|
||||
- `.trae/specs/`: feature 级规格、任务拆解和验收清单
|
||||
|
||||
更详细映射看 `docs/ai/CODEBASE_MAP.md`。
|
||||
|
||||
## 3. 常用命令
|
||||
|
||||
```bash
|
||||
dotnet restore
|
||||
dotnet build LanMountainDesktop.slnx -c Debug
|
||||
dotnet run --project LanMountainDesktop/LanMountainDesktop.csproj
|
||||
dotnet test LanMountainDesktop.slnx -c Debug
|
||||
```
|
||||
|
||||
插件本地包生成:
|
||||
|
||||
```powershell
|
||||
./scripts/Pack-PluginPackages.ps1
|
||||
```
|
||||
|
||||
## 4. 改动前后必做检查
|
||||
|
||||
改动前:
|
||||
|
||||
- 先确认需求是否已经在 `.trae/specs/` 中存在
|
||||
- 先确认产品、架构、专题规范分别以哪份文档为准
|
||||
- 避免沿用旧根目录产品文档中的过时事实
|
||||
|
||||
改动后:
|
||||
|
||||
- 至少检查构建和与改动相关的测试
|
||||
- 如果行为、流程、边界或命令变化,更新对应文档
|
||||
- 如果是新功能或行为调整,补齐或更新 `.trae/specs/<feature>/`
|
||||
|
||||
## 5. 高频区域注意事项
|
||||
|
||||
### UI
|
||||
|
||||
- 主题、资源和视觉语义优先遵守 `docs/VISUAL_SPEC.md` 与 `docs/CORNER_RADIUS_SPEC.md`
|
||||
- 设置页相关改动通常同时落在 `Views/`、`ViewModels/`、`Services/` 和 `.trae/specs/`
|
||||
- UI 启动与窗口生命周期主线在 `Program.cs` 和 `App.axaml.cs`
|
||||
|
||||
### 插件
|
||||
|
||||
- SDK 公共 API 以 `LanMountainDesktop.PluginSdk/` 为准
|
||||
- 共享契约以 `LanMountainDesktop.Shared.Contracts/` 为准
|
||||
- market 数据来源默认是兄弟仓库 `..\\LanAirApp`
|
||||
- 迁移或 breaking change 优先同步 `docs/PLUGIN_SDK_V4_MIGRATION.md`
|
||||
|
||||
### 设置与主题
|
||||
|
||||
- 设置持久化和 scope 变化优先检查 `LanMountainDesktop.Settings.Core/`
|
||||
- 外观、圆角、主题资源优先检查 `LanMountainDesktop.Appearance/` 与专题规范
|
||||
|
||||
## 6. 权威来源
|
||||
|
||||
- 产品定位:`docs/PRODUCT.md`
|
||||
- 架构与模块职责:`docs/ARCHITECTURE.md`
|
||||
- 运行、构建、测试、打包:`docs/DEVELOPMENT.md`
|
||||
- feature 规格:`.trae/specs/`
|
||||
- 视觉规范:`docs/VISUAL_SPEC.md`
|
||||
- 圆角规范:`docs/CORNER_RADIUS_SPEC.md`
|
||||
- 生态边界:`docs/ECOSYSTEM_BOUNDARIES.md`
|
||||
- SDK v4 迁移:`docs/PLUGIN_SDK_V4_MIGRATION.md`
|
||||
|
||||
如果多个文档都提到同一件事,以 `docs/ai/DOC_SOURCES.md` 列出的权威来源为准。
|
||||
8
Directory.Build.props
Normal file
8
Directory.Build.props
Normal file
@@ -0,0 +1,8 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.0.0</Version>
|
||||
<TargetFramework Condition="'$(TargetFramework)' == ''">net10.0</TargetFramework>
|
||||
<Nullable Condition="'$(Nullable)' == ''">enable</Nullable>
|
||||
<ImplicitUsings Condition="'$(ImplicitUsings)' == ''">enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,33 +0,0 @@
|
||||
# LanAirApp (Mirror)
|
||||
|
||||
## 中文
|
||||
|
||||
这里的 `LanAirApp/` 是放在宿主仓库里的镜像副本,只用于本地联调和工作区构建,不是插件市场或插件开发资料的最终权威来源。
|
||||
|
||||
### 这份镜像的角色
|
||||
|
||||
- 提供本地工作区里的 `airappmarket` 索引副本
|
||||
- 提供插件文档、工具和样例镜像,便于和宿主一起联调
|
||||
- 不承担宿主运行时职责
|
||||
|
||||
### 权威来源
|
||||
|
||||
- 插件市场与开发文档:独立 `LanAirApp` 仓库
|
||||
- 权威示例插件:独立 `LanMountainDesktop.SamplePlugin`
|
||||
- 本目录中的 `samples/LanMountainDesktop.SamplePlugin` 只是镜像模板副本
|
||||
|
||||
## English
|
||||
|
||||
This `LanAirApp/` directory is a mirror that lives inside the host repository. It exists for local workspace integration and build convenience only. It is not the final authority for the plugin market or developer-facing plugin materials.
|
||||
|
||||
### Role of this mirror
|
||||
|
||||
- keep a local copy of the `airappmarket` index for workspace integration
|
||||
- keep mirrored docs, tools, and sample templates for local development
|
||||
- avoid duplicating host runtime responsibilities
|
||||
|
||||
### Sources of truth
|
||||
|
||||
- Plugin market and developer docs: standalone `LanAirApp`
|
||||
- Authoritative sample plugin: standalone `LanMountainDesktop.SamplePlugin`
|
||||
- `samples/LanMountainDesktop.SamplePlugin` in this mirror is template/mirror content only
|
||||
@@ -1,16 +0,0 @@
|
||||
# 插件开发指南
|
||||
|
||||
## 中文
|
||||
|
||||
使用 `LanMountainDesktop.PluginSdk` 开发插件时,至少需要准备:
|
||||
|
||||
- `plugin.json`
|
||||
- 插件入口程序集
|
||||
- 入口类
|
||||
- 本地化资源
|
||||
|
||||
推荐从示例插件开始,先完成清单、入口、设置页和桌面组件,再逐步扩展业务逻辑。
|
||||
|
||||
## English
|
||||
|
||||
To build a plugin with `LanMountainDesktop.PluginSdk`, prepare the manifest, plugin assembly, entrance class, and localization resources first.
|
||||
@@ -1,14 +0,0 @@
|
||||
# 插件打包指南
|
||||
|
||||
## 中文
|
||||
|
||||
阑山桌面插件的标准安装格式为 `.laapp`。插件项目应在仓库根目录提供:
|
||||
|
||||
- `.laapp` 安装包
|
||||
- `README.md`
|
||||
|
||||
官方市场索引只负责记录链接和校验信息。
|
||||
|
||||
## English
|
||||
|
||||
The standard package format is `.laapp`. Plugin repositories should keep the package and `README.md` in the repository root, while the official market index stores metadata and validation data.
|
||||
@@ -1,30 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>1.0.0</Version>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<OutputPath>bin\$(Configuration)\$(TargetFramework)\content\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<PluginPackageOutputDirectory>$(MSBuildThisFileDirectory)artifacts\Packages\</PluginPackageOutputDirectory>
|
||||
<PluginPackagePath>$(PluginPackageOutputDirectory)$(AssemblyName).$(Version).laapp</PluginPackagePath>
|
||||
<LegacyLoosePluginOutputDirectory>$(MSBuildThisFileDirectory)artifacts\Loose\</LegacyLoosePluginOutputDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" Private="false" />
|
||||
<None Include="plugin.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="Localization\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CreateLaappPackage" AfterTargets="Build">
|
||||
<MakeDir Directories="$(PluginPackageOutputDirectory)" />
|
||||
<RemoveDir Directories="$(LegacyLoosePluginOutputDirectory)" />
|
||||
<Delete Files="$(PluginPackagePath)" TreatErrorsAsWarnings="true" />
|
||||
<ZipDirectory SourceDirectory="$(OutputPath)" DestinationFile="$(PluginPackagePath)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -1,84 +0,0 @@
|
||||
{
|
||||
"settings.page_title": "Plugin Status",
|
||||
"plugin.name": "LanMountain Sample Plugin",
|
||||
"plugin.description": "Example plugin used to validate PluginSdk loading, services, communication, and localization.",
|
||||
"widget.display_name": "Sample Plugin Status Clock",
|
||||
"widget.category": "Plugins",
|
||||
"settings.header.title": "Sample Plugin Capability Inspector",
|
||||
"settings.section.info": "Plugin Info",
|
||||
"settings.section.capabilities": "Accessible Capabilities",
|
||||
"settings.section.status": "Live Runtime Status",
|
||||
"settings.info.plugin_name": "Plugin Name",
|
||||
"settings.info.plugin_id": "Plugin Id",
|
||||
"settings.info.version": "Version",
|
||||
"settings.info.author": "Author",
|
||||
"settings.info.description": "Description",
|
||||
"settings.info.plugin_directory": "Plugin Directory",
|
||||
"settings.info.data_directory": "Data Directory",
|
||||
"settings.info.host_application": "Host Application",
|
||||
"settings.info.host_version": "Host Version",
|
||||
"settings.info.sdk_api_version": "SDK API Version",
|
||||
"settings.info.state_service_resolved": "State Service Resolved",
|
||||
"settings.info.clock_service_resolved": "Clock Service Resolved",
|
||||
"settings.info.message_bus_resolved": "Message Bus Resolved",
|
||||
"settings.info.component_placed": "Component Placed",
|
||||
"settings.info.placed_count": "Placed Count",
|
||||
"settings.info.preview_count": "Preview Count",
|
||||
"settings.info.placement_ids": "Placement Ids",
|
||||
"settings.info.last_component_id": "Last Component Id",
|
||||
"settings.info.last_cell_size": "Last Cell Size",
|
||||
"settings.info.clock_service_time": "Clock Service Time",
|
||||
"settings.status.updated_at": "Updated: {0}",
|
||||
"status.frontend.title": "Frontend Status",
|
||||
"status.component.title": "Component Status",
|
||||
"status.backend.title": "Backend Status",
|
||||
"status.service.title": "Clock Service",
|
||||
"status.summary.pending": "Pending",
|
||||
"status.summary.attached": "Attached",
|
||||
"status.summary.healthy": "Healthy",
|
||||
"status.summary.faulted": "Faulted",
|
||||
"status.summary.placed": "Placed",
|
||||
"status.summary.preview": "Preview",
|
||||
"status.frontend.detail.pending": "Waiting for a plugin UI surface to connect.",
|
||||
"status.frontend.detail.settings_connected": "Settings page is connected to plugin services and communication.",
|
||||
"status.frontend.detail.widget_connected": "Widget surface is connected to plugin services and communication.",
|
||||
"status.component.detail.pending": "No component instance has been created yet.",
|
||||
"status.component.detail.none": "No component instance is active.",
|
||||
"status.component.detail.preview": "Preview instances: {0}; no placed desktop instance is active yet.",
|
||||
"status.component.detail.placed": "Placed count: {0}; preview count: {1}; placements: {2}",
|
||||
"status.backend.detail.pending": "Plugin initialization is in progress.",
|
||||
"status.backend.detail.log_written": "Initialization log written to: {0}",
|
||||
"status.backend.detail.log_write_failed": "Initialization log write failed: {0}",
|
||||
"status.service.detail.pending": "Clock service is not attached yet.",
|
||||
"status.service.detail.attached": "Clock service was attached and is waiting for the first tick.",
|
||||
"status.service.detail.running": "Clock service is running. Current service time: {0}",
|
||||
"status.service.detail.write_failed": "Clock state write failed: {0}",
|
||||
"capability.manifest.title": "IPluginContext.Manifest",
|
||||
"capability.manifest.detail": "Readable. Current plugin id: {0}; version: {1}.",
|
||||
"capability.directories.title": "IPluginContext.PluginDirectory / DataDirectory",
|
||||
"capability.directories.detail": "Readable. Plugin directory: {0}; data directory: {1}.",
|
||||
"capability.properties.title": "IPluginContext.Properties",
|
||||
"capability.properties.detail": "Readable. Host properties currently exposed: {0}.",
|
||||
"capability.get_service.title": "IPluginContext.GetService<T>()",
|
||||
"capability.get_service.detail": "Callable. State service resolved: {0}; clock service resolved: {1}; message bus resolved: {2}.",
|
||||
"capability.register_service.title": "IPluginContext.RegisterService<TService>()",
|
||||
"capability.register_service.detail": "Callable during plugin initialization. This sample plugin registers SamplePluginRuntimeStateService and SamplePluginClockService into the plugin service container.",
|
||||
"capability.message_bus.title": "Plugin Communication Bus",
|
||||
"capability.message_bus.detail": "This sample plugin uses IPluginMessageBus to push clock ticks and state change notifications into plugin UI surfaces.",
|
||||
"capability.widget_context.title": "PluginDesktopComponentContext",
|
||||
"capability.widget_context.detail": "Widgets can read ComponentId, PlacementId, CellSize, and call GetService<T>() against the same plugin service container.",
|
||||
"widget.close_desktop.display_name": "Close Desktop",
|
||||
"widget.close_desktop.text": "Close Desktop",
|
||||
"widget.close_desktop.hint": "Exit LanMountainDesktop on click",
|
||||
"widget.close_desktop.unavailable": "Host lifecycle API is unavailable",
|
||||
"widget.close_desktop.failed": "Host rejected the exit request",
|
||||
"widget.subtitle.preview": "Preview surface | placed: {0}",
|
||||
"widget.subtitle.placement": "Placement {0} | placed: {1}",
|
||||
"common.dev": "dev",
|
||||
"common.none": "(none)",
|
||||
"common.unknown": "(unknown)",
|
||||
"common.true": "true",
|
||||
"common.false": "false",
|
||||
"common.yes": "Yes",
|
||||
"common.no": "No"
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"settings.page_title": "插件状态",
|
||||
"plugin.name": "阑山示例插件",
|
||||
"plugin.description": "用于验证 PluginSdk 加载、服务、通信与本地化能力的示例插件。",
|
||||
"widget.display_name": "示例插件状态时钟",
|
||||
"widget.category": "插件",
|
||||
"settings.header.title": "示例插件能力检查器",
|
||||
"settings.section.info": "插件信息",
|
||||
"settings.section.capabilities": "可访问能力",
|
||||
"settings.section.status": "实时运行状态",
|
||||
"settings.info.plugin_name": "插件名称",
|
||||
"settings.info.plugin_id": "插件 Id",
|
||||
"settings.info.version": "版本",
|
||||
"settings.info.author": "作者",
|
||||
"settings.info.description": "描述",
|
||||
"settings.info.plugin_directory": "插件目录",
|
||||
"settings.info.data_directory": "数据目录",
|
||||
"settings.info.host_application": "宿主应用",
|
||||
"settings.info.host_version": "宿主版本",
|
||||
"settings.info.sdk_api_version": "SDK API 版本",
|
||||
"settings.info.state_service_resolved": "状态服务已解析",
|
||||
"settings.info.clock_service_resolved": "时钟服务已解析",
|
||||
"settings.info.message_bus_resolved": "消息总线已解析",
|
||||
"settings.info.component_placed": "组件是否已放置",
|
||||
"settings.info.placed_count": "已放置数量",
|
||||
"settings.info.preview_count": "预览数量",
|
||||
"settings.info.placement_ids": "放置位置 Id",
|
||||
"settings.info.last_component_id": "最近组件 Id",
|
||||
"settings.info.last_cell_size": "最近单元尺寸",
|
||||
"settings.info.clock_service_time": "时钟服务时间",
|
||||
"settings.status.updated_at": "更新时间:{0}",
|
||||
"status.frontend.title": "前端状态",
|
||||
"status.component.title": "组件状态",
|
||||
"status.backend.title": "后端状态",
|
||||
"status.service.title": "时钟服务",
|
||||
"status.summary.pending": "等待中",
|
||||
"status.summary.attached": "已挂接",
|
||||
"status.summary.healthy": "正常",
|
||||
"status.summary.faulted": "异常",
|
||||
"status.summary.placed": "已放置",
|
||||
"status.summary.preview": "预览中",
|
||||
"status.frontend.detail.pending": "等待插件界面接入。",
|
||||
"status.frontend.detail.settings_connected": "设置页已接入插件服务与通信。",
|
||||
"status.frontend.detail.widget_connected": "组件界面已接入插件服务与通信。",
|
||||
"status.component.detail.pending": "当前还没有创建组件实例。",
|
||||
"status.component.detail.none": "当前没有活动中的组件实例。",
|
||||
"status.component.detail.preview": "当前预览实例数量:{0};尚未有已放置的桌面实例。",
|
||||
"status.component.detail.placed": "已放置数量:{0};预览数量:{1};放置位置:{2}",
|
||||
"status.backend.detail.pending": "插件初始化进行中。",
|
||||
"status.backend.detail.log_written": "初始化日志已写入:{0}",
|
||||
"status.backend.detail.log_write_failed": "初始化日志写入失败:{0}",
|
||||
"status.service.detail.pending": "时钟服务尚未挂接。",
|
||||
"status.service.detail.attached": "时钟服务已挂接,正在等待第一次心跳。",
|
||||
"status.service.detail.running": "时钟服务运行中,当前服务时间:{0}",
|
||||
"status.service.detail.write_failed": "时钟状态写入失败:{0}",
|
||||
"capability.manifest.title": "IPluginContext.Manifest",
|
||||
"capability.manifest.detail": "可读取。当前插件 id:{0};版本:{1}。",
|
||||
"capability.directories.title": "IPluginContext.PluginDirectory / DataDirectory",
|
||||
"capability.directories.detail": "可读取。插件目录:{0};数据目录:{1}。",
|
||||
"capability.properties.title": "IPluginContext.Properties",
|
||||
"capability.properties.detail": "可读取。宿主当前暴露的属性:{0}。",
|
||||
"capability.get_service.title": "IPluginContext.GetService<T>()",
|
||||
"capability.get_service.detail": "可调用。状态服务已解析:{0};时钟服务已解析:{1};消息总线已解析:{2}。",
|
||||
"capability.register_service.title": "IPluginContext.RegisterService<TService>()",
|
||||
"capability.register_service.detail": "可在插件初始化阶段调用。这个示例插件会把 SamplePluginRuntimeStateService 和 SamplePluginClockService 注册进插件服务容器。",
|
||||
"capability.message_bus.title": "插件通信总线",
|
||||
"capability.message_bus.detail": "这个示例插件通过 IPluginMessageBus 向插件 UI 推送时钟心跳和状态变化通知。",
|
||||
"capability.widget_context.title": "PluginDesktopComponentContext",
|
||||
"capability.widget_context.detail": "组件可以读取 ComponentId、PlacementId、CellSize,并能在同一个插件服务容器上调用 GetService<T>()。",
|
||||
"widget.subtitle.preview": "预览界面 | 已放置:{0}",
|
||||
"widget.subtitle.placement": "位置 {0} | 已放置:{1}",
|
||||
"common.dev": "开发版",
|
||||
"common.none": "(无)",
|
||||
"common.unknown": "(未知)",
|
||||
"common.true": "是",
|
||||
"common.false": "否",
|
||||
"common.yes": "是",
|
||||
"common.no": "否"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# LanMountainDesktop.SamplePlugin
|
||||
|
||||
## 中文
|
||||
|
||||
这是阑山桌面的标准示例插件,用于演示插件清单、设置页、桌面组件、服务注册、本地化和 `.laapp` 打包流程。
|
||||
|
||||
## English
|
||||
|
||||
This is the standard sample plugin used to demonstrate manifests, settings pages, desktop components, service registration, localization, and `.laapp` packaging.
|
||||
@@ -1,100 +0,0 @@
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.SamplePlugin;
|
||||
|
||||
[PluginEntrance]
|
||||
public sealed class SamplePlugin : PluginBase, IDisposable
|
||||
{
|
||||
private SamplePluginRuntimeStateService? _stateService;
|
||||
private SamplePluginClockService? _clockService;
|
||||
|
||||
public override void Initialize(IPluginContext context)
|
||||
{
|
||||
Directory.CreateDirectory(context.DataDirectory);
|
||||
var localizer = PluginLocalizer.Create(context);
|
||||
|
||||
var hostName = GetHostProperty(context, PluginHostPropertyKeys.HostApplicationName, "UnknownHost");
|
||||
var hostVersion = GetHostProperty(context, PluginHostPropertyKeys.HostVersion, "UnknownVersion");
|
||||
var sdkApiVersion = GetHostProperty(context, PluginHostPropertyKeys.PluginSdkApiVersion, "UnknownApiVersion");
|
||||
var messageBus = context.GetService<IPluginMessageBus>()
|
||||
?? throw new InvalidOperationException("Plugin message bus is not available.");
|
||||
|
||||
_stateService = new SamplePluginRuntimeStateService(
|
||||
context.Manifest,
|
||||
context.PluginDirectory,
|
||||
context.DataDirectory,
|
||||
hostName,
|
||||
hostVersion,
|
||||
sdkApiVersion,
|
||||
messageBus,
|
||||
localizer);
|
||||
context.RegisterService(_stateService);
|
||||
|
||||
_clockService = new SamplePluginClockService(context.DataDirectory, _stateService, messageBus, localizer);
|
||||
context.RegisterService(_clockService);
|
||||
_stateService.AttachClockService(_clockService);
|
||||
|
||||
var logPath = Path.Combine(context.DataDirectory, "sample-plugin.log");
|
||||
var initMessage =
|
||||
$"[{DateTimeOffset.UtcNow:O}] {context.Manifest.Name} initialized in {hostName} (plugin version {context.Manifest.Version ?? "dev"}).";
|
||||
|
||||
try
|
||||
{
|
||||
File.AppendAllText(logPath, initMessage + Environment.NewLine);
|
||||
_stateService.MarkBackendReady(localizer.Format(
|
||||
"status.backend.detail.log_written",
|
||||
"Initialization log written: {0}",
|
||||
logPath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_stateService.MarkBackendFaulted(localizer.Format(
|
||||
"status.backend.detail.log_write_failed",
|
||||
"Initialization log failed: {0}",
|
||||
ex.Message));
|
||||
throw;
|
||||
}
|
||||
|
||||
_clockService.Start();
|
||||
|
||||
context.RegisterDesktopComponent(new PluginDesktopComponentRegistration(
|
||||
"LanMountainDesktop.SamplePlugin.StatusClock",
|
||||
localizer.GetString("widget.display_name", "Sample Plugin Status Clock"),
|
||||
widgetContext => new SamplePluginStatusClockWidget(widgetContext),
|
||||
iconKey: "PuzzlePiece",
|
||||
category: localizer.GetString("widget.category", "Plugins"),
|
||||
minWidthCells: 4,
|
||||
minHeightCells: 4,
|
||||
allowDesktopPlacement: true,
|
||||
allowStatusBarPlacement: false,
|
||||
resizeMode: PluginDesktopComponentResizeMode.Proportional,
|
||||
cornerRadiusResolver: cellSize => Math.Clamp(cellSize * 0.34, 18, 34)));
|
||||
|
||||
context.RegisterDesktopComponent(new PluginDesktopComponentRegistration(
|
||||
"LanMountainDesktop.SamplePlugin.CloseDesktop",
|
||||
localizer.GetString("widget.close_desktop.display_name", "Close Desktop"),
|
||||
widgetContext => new SamplePluginCloseDesktopWidget(widgetContext),
|
||||
iconKey: "DismissCircle",
|
||||
category: localizer.GetString("widget.category", "Plugins"),
|
||||
minWidthCells: 2,
|
||||
minHeightCells: 1,
|
||||
allowDesktopPlacement: true,
|
||||
allowStatusBarPlacement: false,
|
||||
resizeMode: PluginDesktopComponentResizeMode.Free,
|
||||
cornerRadiusResolver: cellSize => Math.Clamp(cellSize * 0.28, 14, 22)));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_clockService?.Dispose();
|
||||
_clockService = null;
|
||||
_stateService = null;
|
||||
}
|
||||
|
||||
private static string GetHostProperty(IPluginContext context, string key, string fallback)
|
||||
{
|
||||
return context.TryGetProperty<string>(key, out var value) && !string.IsNullOrWhiteSpace(value)
|
||||
? value
|
||||
: fallback;
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.SamplePlugin;
|
||||
|
||||
internal sealed class SamplePluginCloseDesktopWidget : Border
|
||||
{
|
||||
private readonly PluginLocalizer _localizer;
|
||||
private readonly IHostApplicationLifecycle? _hostApplicationLifecycle;
|
||||
private readonly TextBlock _titleTextBlock;
|
||||
private readonly TextBlock _statusTextBlock;
|
||||
|
||||
public SamplePluginCloseDesktopWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
_localizer = PluginLocalizer.Create(context);
|
||||
_hostApplicationLifecycle = context.GetService<IHostApplicationLifecycle>();
|
||||
|
||||
_titleTextBlock = new TextBlock
|
||||
{
|
||||
Text = T("widget.close_desktop.text", "关闭桌面"),
|
||||
Foreground = Brushes.White,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
_statusTextBlock = new TextBlock
|
||||
{
|
||||
Text = _hostApplicationLifecycle is null
|
||||
? T("widget.close_desktop.unavailable", "宿主未提供退出接口")
|
||||
: T("widget.close_desktop.hint", "点击后退出阑山桌面"),
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFD4E7F6")),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
var contentGrid = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("Auto,*"),
|
||||
ColumnSpacing = 14,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
CreateIconShell(),
|
||||
new StackPanel
|
||||
{
|
||||
Spacing = 2,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
_titleTextBlock,
|
||||
_statusTextBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Grid.SetColumn(contentGrid.Children[1], 1);
|
||||
|
||||
var actionButton = new Button
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Stretch,
|
||||
HorizontalContentAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalContentAlignment = VerticalAlignment.Stretch,
|
||||
Background = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Padding = new Thickness(0),
|
||||
IsEnabled = _hostApplicationLifecycle is not null,
|
||||
Content = contentGrid
|
||||
};
|
||||
actionButton.Click += OnButtonClick;
|
||||
|
||||
Background = new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops =
|
||||
[
|
||||
new GradientStop(Color.Parse("#FF0B1220"), 0),
|
||||
new GradientStop(Color.Parse("#FF172554"), 0.55),
|
||||
new GradientStop(Color.Parse("#FF7F1D1D"), 1)
|
||||
]
|
||||
};
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#66FB7185"));
|
||||
BorderThickness = new Thickness(1);
|
||||
CornerRadius = new CornerRadius(18);
|
||||
Padding = new Thickness(14, 10);
|
||||
Child = actionButton;
|
||||
|
||||
SizeChanged += OnSizeChanged;
|
||||
ApplyScale();
|
||||
}
|
||||
|
||||
private Border CreateIconShell()
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Width = 36,
|
||||
Height = 36,
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Background = new SolidColorBrush(Color.Parse("#33F87171")),
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#88FCA5A5")),
|
||||
BorderThickness = new Thickness(1),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = "⏻",
|
||||
FontSize = 18,
|
||||
Foreground = Brushes.White,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextAlignment = TextAlignment.Center
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void OnButtonClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_hostApplicationLifecycle?.TryExit(new HostApplicationLifecycleRequest(
|
||||
Source: "SamplePlugin.CloseDesktopWidget",
|
||||
Reason: "User invoked the sample plugin close-desktop widget.")) == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_statusTextBlock.Text = T("widget.close_desktop.failed", "宿主未接受退出请求");
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ApplyScale();
|
||||
}
|
||||
|
||||
private void ApplyScale()
|
||||
{
|
||||
var basis = Bounds.Height > 1 ? Bounds.Height : 72;
|
||||
Padding = new Thickness(Math.Clamp(basis * 0.18, 12, 18), Math.Clamp(basis * 0.14, 8, 14));
|
||||
CornerRadius = new CornerRadius(Math.Clamp(basis * 0.32, 16, 24));
|
||||
|
||||
if (Child is not Button actionButton || actionButton.Content is not Grid contentGrid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (contentGrid.Children[0] is Border iconShell)
|
||||
{
|
||||
var iconSize = Math.Clamp(basis * 0.58, 28, 40);
|
||||
iconShell.Width = iconSize;
|
||||
iconShell.Height = iconSize;
|
||||
if (iconShell.Child is TextBlock iconText)
|
||||
{
|
||||
iconText.FontSize = Math.Clamp(iconSize * 0.5, 14, 20);
|
||||
}
|
||||
}
|
||||
|
||||
_titleTextBlock.FontSize = Math.Clamp(basis * 0.28, 14, 20);
|
||||
_statusTextBlock.FontSize = Math.Clamp(basis * 0.18, 10, 13);
|
||||
}
|
||||
|
||||
private string T(string key, string fallback)
|
||||
{
|
||||
return _localizer.GetString(key, fallback);
|
||||
}
|
||||
}
|
||||
@@ -1,524 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.SamplePlugin;
|
||||
|
||||
internal enum SamplePluginHealthState
|
||||
{
|
||||
Healthy,
|
||||
Pending,
|
||||
Faulted
|
||||
}
|
||||
|
||||
internal sealed record SamplePluginStatusEntry(
|
||||
string Key,
|
||||
string Title,
|
||||
SamplePluginHealthState State,
|
||||
string Summary,
|
||||
string Detail,
|
||||
DateTimeOffset UpdatedAt);
|
||||
|
||||
internal sealed record SamplePluginCapabilityItem(
|
||||
string Title,
|
||||
string Detail);
|
||||
|
||||
internal sealed record SamplePluginRuntimeSnapshot(
|
||||
PluginManifest Manifest,
|
||||
string PluginDirectory,
|
||||
string DataDirectory,
|
||||
string HostApplicationName,
|
||||
string HostVersion,
|
||||
string SdkApiVersion,
|
||||
IReadOnlyList<SamplePluginStatusEntry> StatusEntries,
|
||||
bool HasPlacedComponent,
|
||||
int PlacedCount,
|
||||
int PreviewCount,
|
||||
IReadOnlyList<string> PlacementIds,
|
||||
string? LastComponentId,
|
||||
double LastCellSize,
|
||||
DateTimeOffset? ServiceClockTime);
|
||||
|
||||
internal sealed record SamplePluginClockTickMessage(DateTimeOffset CurrentTime);
|
||||
|
||||
internal sealed record SamplePluginStateChangedMessage(string Reason);
|
||||
|
||||
internal sealed record SamplePluginComponentInstance(
|
||||
string ComponentId,
|
||||
string? PlacementId,
|
||||
double CellSize)
|
||||
{
|
||||
public bool IsPlaced => !string.IsNullOrWhiteSpace(PlacementId);
|
||||
}
|
||||
|
||||
internal sealed class SamplePluginRuntimeStateService
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly IPluginMessageBus _messageBus;
|
||||
private readonly Dictionary<string, SamplePluginComponentInstance> _componentInstances =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly PluginManifest _manifest;
|
||||
private readonly string _pluginDirectory;
|
||||
private readonly string _dataDirectory;
|
||||
private readonly string _hostApplicationName;
|
||||
private readonly string _hostVersion;
|
||||
private readonly string _sdkApiVersion;
|
||||
private readonly PluginLocalizer _localizer;
|
||||
|
||||
private SamplePluginStatusEntry _frontend;
|
||||
private SamplePluginStatusEntry _component;
|
||||
private SamplePluginStatusEntry _backend;
|
||||
private SamplePluginStatusEntry _service;
|
||||
private string? _lastComponentId;
|
||||
private double _lastCellSize;
|
||||
private DateTimeOffset? _serviceClockTime;
|
||||
|
||||
public SamplePluginRuntimeStateService(
|
||||
PluginManifest manifest,
|
||||
string pluginDirectory,
|
||||
string dataDirectory,
|
||||
string hostApplicationName,
|
||||
string hostVersion,
|
||||
string sdkApiVersion,
|
||||
IPluginMessageBus messageBus,
|
||||
PluginLocalizer localizer)
|
||||
{
|
||||
_manifest = manifest;
|
||||
_pluginDirectory = pluginDirectory;
|
||||
_dataDirectory = dataDirectory;
|
||||
_hostApplicationName = hostApplicationName;
|
||||
_hostVersion = hostVersion;
|
||||
_sdkApiVersion = sdkApiVersion;
|
||||
_messageBus = messageBus;
|
||||
_localizer = localizer;
|
||||
|
||||
_frontend = CreateEntry(
|
||||
"frontend",
|
||||
T("status.frontend.title", "前端状态"),
|
||||
SamplePluginHealthState.Pending,
|
||||
T("status.summary.pending", "等待中"),
|
||||
T("status.frontend.detail.pending", "等待插件界面接入。"));
|
||||
|
||||
_component = CreateEntry(
|
||||
"component",
|
||||
T("status.component.title", "组件状态"),
|
||||
SamplePluginHealthState.Pending,
|
||||
T("status.summary.pending", "等待中"),
|
||||
T("status.component.detail.pending", "当前还没有创建组件实例。"));
|
||||
|
||||
_backend = CreateEntry(
|
||||
"backend",
|
||||
T("status.backend.title", "后端状态"),
|
||||
SamplePluginHealthState.Pending,
|
||||
T("status.summary.pending", "等待中"),
|
||||
T("status.backend.detail.pending", "插件初始化进行中。"));
|
||||
|
||||
_service = CreateEntry(
|
||||
"service",
|
||||
T("status.service.title", "时钟服务"),
|
||||
SamplePluginHealthState.Pending,
|
||||
T("status.summary.pending", "等待中"),
|
||||
T("status.service.detail.pending", "时钟服务尚未挂接。"));
|
||||
}
|
||||
|
||||
public void AttachClockService(SamplePluginClockService clockService)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(clockService);
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_serviceClockTime = clockService.CurrentTime;
|
||||
_service = CreateEntry(
|
||||
"service",
|
||||
T("status.service.title", "时钟服务"),
|
||||
SamplePluginHealthState.Pending,
|
||||
T("status.summary.attached", "已挂接"),
|
||||
T("status.service.detail.attached", "时钟服务已挂接,正在等待第一次心跳。"));
|
||||
}
|
||||
|
||||
PublishStateChanged("Clock service attached");
|
||||
}
|
||||
|
||||
public void MarkFrontendReady(string detail)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_frontend = CreateEntry(
|
||||
"frontend",
|
||||
T("status.frontend.title", "前端状态"),
|
||||
SamplePluginHealthState.Healthy,
|
||||
T("status.summary.healthy", "正常"),
|
||||
detail);
|
||||
}
|
||||
|
||||
PublishStateChanged("Frontend updated");
|
||||
}
|
||||
|
||||
public void MarkBackendReady(string detail)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_backend = CreateEntry(
|
||||
"backend",
|
||||
T("status.backend.title", "后端状态"),
|
||||
SamplePluginHealthState.Healthy,
|
||||
T("status.summary.healthy", "正常"),
|
||||
detail);
|
||||
}
|
||||
|
||||
PublishStateChanged("Backend updated");
|
||||
}
|
||||
|
||||
public void MarkBackendFaulted(string detail)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_backend = CreateEntry(
|
||||
"backend",
|
||||
T("status.backend.title", "后端状态"),
|
||||
SamplePluginHealthState.Faulted,
|
||||
T("status.summary.faulted", "异常"),
|
||||
detail);
|
||||
}
|
||||
|
||||
PublishStateChanged("Backend faulted");
|
||||
}
|
||||
|
||||
public void MarkClockServiceTick(DateTimeOffset currentTime)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_serviceClockTime = currentTime;
|
||||
_service = CreateEntry(
|
||||
"service",
|
||||
T("status.service.title", "时钟服务"),
|
||||
SamplePluginHealthState.Healthy,
|
||||
T("status.summary.healthy", "正常"),
|
||||
Tf(
|
||||
"status.service.detail.running",
|
||||
"时钟服务运行中,当前服务时间:{0}",
|
||||
currentTime.LocalDateTime.ToString("HH:mm:ss")));
|
||||
}
|
||||
|
||||
PublishStateChanged("Clock service tick");
|
||||
}
|
||||
|
||||
public void MarkClockServiceFaulted(string detail)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_service = CreateEntry(
|
||||
"service",
|
||||
T("status.service.title", "时钟服务"),
|
||||
SamplePluginHealthState.Faulted,
|
||||
T("status.summary.faulted", "异常"),
|
||||
detail);
|
||||
}
|
||||
|
||||
PublishStateChanged("Clock service faulted");
|
||||
}
|
||||
|
||||
public string RegisterComponentInstance(string componentId, string? placementId, double cellSize)
|
||||
{
|
||||
var instanceId = Guid.NewGuid().ToString("N");
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_componentInstances[instanceId] = new SamplePluginComponentInstance(componentId, placementId, cellSize);
|
||||
_lastComponentId = componentId;
|
||||
_lastCellSize = cellSize;
|
||||
UpdateComponentStatusNoLock();
|
||||
}
|
||||
|
||||
PublishStateChanged("Component attached");
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
public void UnregisterComponentInstance(string instanceId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(instanceId);
|
||||
|
||||
var removed = false;
|
||||
lock (_gate)
|
||||
{
|
||||
removed = _componentInstances.Remove(instanceId);
|
||||
if (removed)
|
||||
{
|
||||
UpdateComponentStatusNoLock();
|
||||
}
|
||||
}
|
||||
|
||||
if (removed)
|
||||
{
|
||||
PublishStateChanged("Component detached");
|
||||
}
|
||||
}
|
||||
|
||||
public SamplePluginRuntimeSnapshot GetSnapshot()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var placementIds = _componentInstances.Values
|
||||
.Where(instance => instance.IsPlaced)
|
||||
.Select(instance => instance.PlacementId!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var previewCount = _componentInstances.Values.Count(instance => !instance.IsPlaced);
|
||||
|
||||
return new SamplePluginRuntimeSnapshot(
|
||||
_manifest,
|
||||
_pluginDirectory,
|
||||
_dataDirectory,
|
||||
_hostApplicationName,
|
||||
_hostVersion,
|
||||
_sdkApiVersion,
|
||||
[_frontend, _component, _backend, _service],
|
||||
placementIds.Length > 0,
|
||||
placementIds.Length,
|
||||
previewCount,
|
||||
placementIds,
|
||||
_lastComponentId,
|
||||
_lastCellSize,
|
||||
_serviceClockTime);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<SamplePluginCapabilityItem> GetCapabilities(
|
||||
IPluginContext context,
|
||||
bool hasStateService,
|
||||
bool hasClockService,
|
||||
bool hasMessageBus)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var propertyNames = context.Properties.Count == 0
|
||||
? T("common.none", "(无)")
|
||||
: string.Join(", ", context.Properties.Keys.OrderBy(key => key, StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
return
|
||||
[
|
||||
new SamplePluginCapabilityItem(
|
||||
T("capability.manifest.title", "IPluginContext.Manifest"),
|
||||
Tf(
|
||||
"capability.manifest.detail",
|
||||
"可读取。当前插件 id:{0};版本:{1}。",
|
||||
context.Manifest.Id,
|
||||
context.Manifest.Version ?? T("common.dev", "开发版"))),
|
||||
new SamplePluginCapabilityItem(
|
||||
T("capability.directories.title", "IPluginContext.PluginDirectory / DataDirectory"),
|
||||
Tf(
|
||||
"capability.directories.detail",
|
||||
"可读取。插件目录:{0};数据目录:{1}。",
|
||||
context.PluginDirectory,
|
||||
context.DataDirectory)),
|
||||
new SamplePluginCapabilityItem(
|
||||
T("capability.properties.title", "IPluginContext.Properties"),
|
||||
Tf(
|
||||
"capability.properties.detail",
|
||||
"可读取。宿主当前暴露的属性:{0}。",
|
||||
propertyNames)),
|
||||
new SamplePluginCapabilityItem(
|
||||
T("capability.get_service.title", "IPluginContext.GetService<T>()"),
|
||||
Tf(
|
||||
"capability.get_service.detail",
|
||||
"可调用。状态服务已解析:{0};时钟服务已解析:{1};消息总线已解析:{2}。",
|
||||
FormatBoolean(hasStateService),
|
||||
FormatBoolean(hasClockService),
|
||||
FormatBoolean(hasMessageBus))),
|
||||
new SamplePluginCapabilityItem(
|
||||
T("capability.register_service.title", "IPluginContext.RegisterService<TService>()"),
|
||||
T(
|
||||
"capability.register_service.detail",
|
||||
"可在插件初始化阶段调用。这个示例插件会把 SamplePluginRuntimeStateService 和 SamplePluginClockService 注册进插件服务容器。")),
|
||||
new SamplePluginCapabilityItem(
|
||||
T("capability.message_bus.title", "插件通信总线"),
|
||||
T(
|
||||
"capability.message_bus.detail",
|
||||
"这个示例插件通过 IPluginMessageBus 向插件 UI 推送时钟心跳和状态变化通知。")),
|
||||
new SamplePluginCapabilityItem(
|
||||
T("capability.widget_context.title", "PluginDesktopComponentContext"),
|
||||
T(
|
||||
"capability.widget_context.detail",
|
||||
"组件可以读取 ComponentId、PlacementId、CellSize,并能在同一个插件服务容器上调用 GetService<T>()。"))
|
||||
];
|
||||
}
|
||||
|
||||
private void UpdateComponentStatusNoLock()
|
||||
{
|
||||
var placementIds = _componentInstances.Values
|
||||
.Where(instance => instance.IsPlaced)
|
||||
.Select(instance => instance.PlacementId!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var previewCount = _componentInstances.Values.Count(instance => !instance.IsPlaced);
|
||||
|
||||
if (placementIds.Length > 0)
|
||||
{
|
||||
_component = CreateEntry(
|
||||
"component",
|
||||
T("status.component.title", "组件状态"),
|
||||
SamplePluginHealthState.Healthy,
|
||||
T("status.summary.placed", "已放置"),
|
||||
Tf(
|
||||
"status.component.detail.placed",
|
||||
"已放置数量:{0};预览数量:{1};放置位置:{2}",
|
||||
placementIds.Length,
|
||||
previewCount,
|
||||
string.Join(", ", placementIds)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (previewCount > 0)
|
||||
{
|
||||
_component = CreateEntry(
|
||||
"component",
|
||||
T("status.component.title", "组件状态"),
|
||||
SamplePluginHealthState.Healthy,
|
||||
T("status.summary.preview", "预览中"),
|
||||
Tf(
|
||||
"status.component.detail.preview",
|
||||
"当前预览实例数量:{0};尚未有已放置的桌面实例。",
|
||||
previewCount));
|
||||
return;
|
||||
}
|
||||
|
||||
_component = CreateEntry(
|
||||
"component",
|
||||
T("status.component.title", "组件状态"),
|
||||
SamplePluginHealthState.Pending,
|
||||
T("status.summary.pending", "等待中"),
|
||||
T("status.component.detail.none", "当前没有活动中的组件实例。"));
|
||||
}
|
||||
|
||||
private void PublishStateChanged(string reason)
|
||||
{
|
||||
_messageBus.Publish(new SamplePluginStateChangedMessage(reason));
|
||||
}
|
||||
|
||||
private static SamplePluginStatusEntry CreateEntry(
|
||||
string key,
|
||||
string title,
|
||||
SamplePluginHealthState state,
|
||||
string summary,
|
||||
string detail)
|
||||
{
|
||||
return new SamplePluginStatusEntry(
|
||||
key,
|
||||
title,
|
||||
state,
|
||||
summary,
|
||||
detail,
|
||||
DateTimeOffset.Now);
|
||||
}
|
||||
|
||||
private string T(string key, string fallback)
|
||||
{
|
||||
return _localizer.GetString(key, fallback);
|
||||
}
|
||||
|
||||
private string Tf(string key, string fallback, params object[] args)
|
||||
{
|
||||
return _localizer.Format(key, fallback, args);
|
||||
}
|
||||
|
||||
private string FormatBoolean(bool value)
|
||||
{
|
||||
return value
|
||||
? T("common.true", "是")
|
||||
: T("common.false", "否");
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SamplePluginClockService : IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly string _clockStateFilePath;
|
||||
private readonly SamplePluginRuntimeStateService _stateService;
|
||||
private readonly IPluginMessageBus _messageBus;
|
||||
private readonly PluginLocalizer _localizer;
|
||||
private readonly Timer _timer;
|
||||
private DateTimeOffset _currentTime = DateTimeOffset.Now;
|
||||
private int _disposed;
|
||||
|
||||
public SamplePluginClockService(
|
||||
string dataDirectory,
|
||||
SamplePluginRuntimeStateService stateService,
|
||||
IPluginMessageBus messageBus,
|
||||
PluginLocalizer localizer)
|
||||
{
|
||||
_clockStateFilePath = Path.Combine(dataDirectory, "clock-service.txt");
|
||||
_stateService = stateService;
|
||||
_messageBus = messageBus;
|
||||
_localizer = localizer;
|
||||
_timer = new Timer(OnTimerTick);
|
||||
}
|
||||
|
||||
public DateTimeOffset CurrentTime
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _currentTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
PublishTick();
|
||||
_timer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_timer.Dispose();
|
||||
}
|
||||
|
||||
private void OnTimerTick(object? state)
|
||||
{
|
||||
PublishTick();
|
||||
}
|
||||
|
||||
private void PublishTick()
|
||||
{
|
||||
if (Volatile.Read(ref _disposed) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.Now;
|
||||
lock (_gate)
|
||||
{
|
||||
_currentTime = now;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(
|
||||
_clockStateFilePath,
|
||||
now.ToString("O", CultureInfo.InvariantCulture));
|
||||
_stateService.MarkClockServiceTick(now);
|
||||
_messageBus.Publish(new SamplePluginClockTickMessage(now));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_stateService.MarkClockServiceFaulted(_localizer.Format(
|
||||
"status.service.detail.write_failed",
|
||||
"时钟状态写入失败:{0}",
|
||||
ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,374 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.SamplePlugin;
|
||||
|
||||
internal sealed class SamplePluginSettingsView : UserControl
|
||||
{
|
||||
private readonly IPluginContext _context;
|
||||
private readonly PluginLocalizer _localizer;
|
||||
private readonly SamplePluginRuntimeStateService _stateService;
|
||||
private readonly SamplePluginClockService _clockService;
|
||||
private readonly IPluginMessageBus _messageBus;
|
||||
private readonly StackPanel _pluginInfoPanel = new() { Spacing = 8 };
|
||||
private readonly StackPanel _capabilityPanel = new() { Spacing = 8 };
|
||||
private readonly StackPanel _statusPanel = new() { Spacing = 10 };
|
||||
private readonly List<IDisposable> _subscriptions = [];
|
||||
|
||||
public SamplePluginSettingsView(IPluginContext context)
|
||||
{
|
||||
_context = context;
|
||||
_localizer = PluginLocalizer.Create(context);
|
||||
_stateService = context.GetService<SamplePluginRuntimeStateService>()
|
||||
?? throw new InvalidOperationException("SamplePluginRuntimeStateService is not available.");
|
||||
_clockService = context.GetService<SamplePluginClockService>()
|
||||
?? throw new InvalidOperationException("SamplePluginClockService is not available.");
|
||||
_messageBus = context.GetService<IPluginMessageBus>()
|
||||
?? throw new InvalidOperationException("IPluginMessageBus is not available.");
|
||||
|
||||
_stateService.MarkFrontendReady(T(
|
||||
"status.frontend.detail.settings_connected",
|
||||
"设置页已接入插件服务与通信。"));
|
||||
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
|
||||
Content = new Border
|
||||
{
|
||||
Background = new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops =
|
||||
[
|
||||
new GradientStop(Color.Parse("#1F0B1120"), 0),
|
||||
new GradientStop(Color.Parse("#260C4A6E"), 1)
|
||||
]
|
||||
},
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#6628B2FF")),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(18),
|
||||
Padding = new Thickness(18),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Spacing = 14,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = T("settings.header.title", "示例插件能力检查器"),
|
||||
FontSize = 22,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = Brushes.White
|
||||
},
|
||||
CreateSection(T("settings.section.info", "插件信息"), _pluginInfoPanel),
|
||||
CreateSection(T("settings.section.capabilities", "可访问能力"), _capabilityPanel),
|
||||
CreateSection(T("settings.section.status", "实时运行状态"), _statusPanel)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
SubscribeToPluginBus();
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
foreach (var subscription in _subscriptions)
|
||||
{
|
||||
subscription.Dispose();
|
||||
}
|
||||
|
||||
_subscriptions.Clear();
|
||||
}
|
||||
|
||||
private void SubscribeToPluginBus()
|
||||
{
|
||||
if (_subscriptions.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_subscriptions.Add(_messageBus.Subscribe<SamplePluginClockTickMessage>(_ =>
|
||||
Dispatcher.UIThread.Post(RefreshView)));
|
||||
|
||||
_subscriptions.Add(_messageBus.Subscribe<SamplePluginStateChangedMessage>(_ =>
|
||||
Dispatcher.UIThread.Post(RefreshView)));
|
||||
}
|
||||
|
||||
private void RefreshView()
|
||||
{
|
||||
var snapshot = _stateService.GetSnapshot();
|
||||
RefreshPluginInfo(snapshot);
|
||||
RefreshCapabilities();
|
||||
RefreshStatuses(snapshot);
|
||||
}
|
||||
|
||||
private void RefreshPluginInfo(SamplePluginRuntimeSnapshot snapshot)
|
||||
{
|
||||
_pluginInfoPanel.Children.Clear();
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.plugin_name", "插件名称"),
|
||||
T("plugin.name", snapshot.Manifest.Name)));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.plugin_id", "插件 Id"), snapshot.Manifest.Id));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.version", "版本"), snapshot.Manifest.Version ?? T("common.dev", "开发版")));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.author", "作者"), snapshot.Manifest.Author ?? T("common.none", "(无)")));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.description", "描述"),
|
||||
T("plugin.description", snapshot.Manifest.Description ?? T("common.none", "(无)"))));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.plugin_directory", "插件目录"), snapshot.PluginDirectory));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.data_directory", "数据目录"), snapshot.DataDirectory));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.host_application", "宿主应用"), snapshot.HostApplicationName));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.host_version", "宿主版本"), snapshot.HostVersion));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.sdk_api_version", "SDK API 版本"), snapshot.SdkApiVersion));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.state_service_resolved", "状态服务已解析"),
|
||||
FormatBoolean(_context.GetService<SamplePluginRuntimeStateService>() is not null)));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.clock_service_resolved", "时钟服务已解析"),
|
||||
FormatBoolean(_context.GetService<SamplePluginClockService>() is not null)));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.message_bus_resolved", "消息总线已解析"),
|
||||
FormatBoolean(_context.GetService<IPluginMessageBus>() is not null)));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.component_placed", "组件是否已放置"),
|
||||
snapshot.HasPlacedComponent ? T("common.yes", "是") : T("common.no", "否")));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.placed_count", "已放置数量"), snapshot.PlacedCount.ToString()));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.preview_count", "预览数量"), snapshot.PreviewCount.ToString()));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.placement_ids", "放置位置 Id"),
|
||||
snapshot.PlacementIds.Count == 0 ? T("common.none", "(无)") : string.Join(", ", snapshot.PlacementIds)));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.last_component_id", "最近组件 Id"),
|
||||
snapshot.LastComponentId ?? T("common.none", "(无)")));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.last_cell_size", "最近单元尺寸"),
|
||||
snapshot.LastCellSize > 0 ? $"{snapshot.LastCellSize:F0}px" : T("common.unknown", "(未知)")));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.clock_service_time", "时钟服务时间"),
|
||||
_clockService.CurrentTime.LocalDateTime.ToString("HH:mm:ss")));
|
||||
}
|
||||
|
||||
private void RefreshCapabilities()
|
||||
{
|
||||
var capabilities = _stateService.GetCapabilities(
|
||||
_context,
|
||||
_context.GetService<SamplePluginRuntimeStateService>() is not null,
|
||||
_context.GetService<SamplePluginClockService>() is not null,
|
||||
_context.GetService<IPluginMessageBus>() is not null);
|
||||
|
||||
_capabilityPanel.Children.Clear();
|
||||
foreach (var capability in capabilities)
|
||||
{
|
||||
_capabilityPanel.Children.Add(CreateCapabilityCard(capability));
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshStatuses(SamplePluginRuntimeSnapshot snapshot)
|
||||
{
|
||||
_statusPanel.Children.Clear();
|
||||
|
||||
foreach (var entry in snapshot.StatusEntries)
|
||||
{
|
||||
var palette = GetPalette(entry.State);
|
||||
_statusPanel.Children.Add(new Border
|
||||
{
|
||||
Background = new SolidColorBrush(palette.Background),
|
||||
BorderBrush = new SolidColorBrush(palette.Border),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(12, 10),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Spacing = 4,
|
||||
Children =
|
||||
{
|
||||
CreateStatusHeader(entry, palette),
|
||||
new TextBlock
|
||||
{
|
||||
Text = entry.Detail,
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFE0F2FE")),
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = Tf("settings.status.updated_at", "更新时间:{0}", entry.UpdatedAt.LocalDateTime.ToString("HH:mm:ss")),
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FF93C5FD"))
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private Border CreateSection(string title, Control content)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.Parse("#14000000")),
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#3328B2FF")),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(14),
|
||||
Padding = new Thickness(14),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Spacing = 12,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 16,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = Brushes.White
|
||||
},
|
||||
content
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Control CreateInfoLine(string label, string value)
|
||||
{
|
||||
var grid = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("180,*"),
|
||||
ColumnSpacing = 10
|
||||
};
|
||||
|
||||
var labelText = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFBAE6FD")),
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
var valueText = new TextBlock
|
||||
{
|
||||
Text = value,
|
||||
Foreground = Brushes.White,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
grid.Children.Add(labelText);
|
||||
grid.Children.Add(valueText);
|
||||
Grid.SetColumn(valueText, 1);
|
||||
return grid;
|
||||
}
|
||||
|
||||
private Control CreateCapabilityCard(SamplePluginCapabilityItem item)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.Parse("#0F082F49")),
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#3338BDF8")),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(12, 10),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Spacing = 4,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = item.Title,
|
||||
Foreground = Brushes.White,
|
||||
FontWeight = FontWeight.SemiBold
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = item.Detail,
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFE0F2FE")),
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Control CreateStatusHeader(
|
||||
SamplePluginStatusEntry entry,
|
||||
(Color Background, Color Border, Color Dot) palette)
|
||||
{
|
||||
var grid = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"),
|
||||
ColumnSpacing = 8
|
||||
};
|
||||
|
||||
var dot = new Border
|
||||
{
|
||||
Width = 10,
|
||||
Height = 10,
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Background = new SolidColorBrush(palette.Dot),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
var title = new TextBlock
|
||||
{
|
||||
Text = entry.Title,
|
||||
FontSize = 15,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = Brushes.White
|
||||
};
|
||||
var summary = new TextBlock
|
||||
{
|
||||
Text = entry.Summary,
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFD7F2FF")),
|
||||
HorizontalAlignment = HorizontalAlignment.Right
|
||||
};
|
||||
|
||||
grid.Children.Add(dot);
|
||||
grid.Children.Add(title);
|
||||
grid.Children.Add(summary);
|
||||
Grid.SetColumn(title, 1);
|
||||
Grid.SetColumn(summary, 2);
|
||||
return grid;
|
||||
}
|
||||
|
||||
private static (Color Background, Color Border, Color Dot) GetPalette(SamplePluginHealthState state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
SamplePluginHealthState.Healthy => (
|
||||
Color.Parse("#1F115E59"),
|
||||
Color.Parse("#665EEAD4"),
|
||||
Color.Parse("#5EEAD4")),
|
||||
SamplePluginHealthState.Faulted => (
|
||||
Color.Parse("#291B1B"),
|
||||
Color.Parse("#66F87171"),
|
||||
Color.Parse("#F87171")),
|
||||
_ => (
|
||||
Color.Parse("#2B3A2A0D"),
|
||||
Color.Parse("#66FBBF24"),
|
||||
Color.Parse("#FBBF24"))
|
||||
};
|
||||
}
|
||||
|
||||
private string T(string key, string fallback)
|
||||
{
|
||||
return _localizer.GetString(key, fallback);
|
||||
}
|
||||
|
||||
private string Tf(string key, string fallback, params object[] args)
|
||||
{
|
||||
return _localizer.Format(key, fallback, args);
|
||||
}
|
||||
|
||||
private string FormatBoolean(bool value)
|
||||
{
|
||||
return value
|
||||
? T("common.true", "是")
|
||||
: T("common.false", "否");
|
||||
}
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.SamplePlugin;
|
||||
|
||||
internal sealed class SamplePluginStatusClockWidget : Border
|
||||
{
|
||||
private readonly PluginDesktopComponentContext _context;
|
||||
private readonly PluginLocalizer _localizer;
|
||||
private readonly SamplePluginRuntimeStateService _stateService;
|
||||
private readonly SamplePluginClockService _clockService;
|
||||
private readonly IPluginMessageBus _messageBus;
|
||||
private readonly TextBlock _timeTextBlock;
|
||||
private readonly TextBlock _subtitleTextBlock;
|
||||
private readonly StackPanel _statusPanel;
|
||||
private readonly Border _statusHost;
|
||||
private readonly List<IDisposable> _subscriptions = [];
|
||||
private string? _instanceId;
|
||||
|
||||
public SamplePluginStatusClockWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
_context = context;
|
||||
_localizer = PluginLocalizer.Create(context);
|
||||
_stateService = context.GetService<SamplePluginRuntimeStateService>()
|
||||
?? throw new InvalidOperationException("SamplePluginRuntimeStateService is not available.");
|
||||
_clockService = context.GetService<SamplePluginClockService>()
|
||||
?? throw new InvalidOperationException("SamplePluginClockService is not available.");
|
||||
_messageBus = context.GetService<IPluginMessageBus>()
|
||||
?? throw new InvalidOperationException("IPluginMessageBus is not available.");
|
||||
|
||||
_timeTextBlock = new TextBlock
|
||||
{
|
||||
Foreground = Brushes.White,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Left
|
||||
};
|
||||
_subtitleTextBlock = new TextBlock
|
||||
{
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFBFE9FF")),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
_statusPanel = new StackPanel
|
||||
{
|
||||
Spacing = 8
|
||||
};
|
||||
_statusHost = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.Parse("#1F082F49")),
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#5538BDF8")),
|
||||
BorderThickness = new Thickness(1),
|
||||
Child = _statusPanel
|
||||
};
|
||||
|
||||
Background = new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops =
|
||||
[
|
||||
new GradientStop(Color.Parse("#FF07111F"), 0),
|
||||
new GradientStop(Color.Parse("#FF0C4A6E"), 0.55),
|
||||
new GradientStop(Color.Parse("#FF0EA5E9"), 1)
|
||||
]
|
||||
};
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#6648C7FF"));
|
||||
BorderThickness = new Thickness(1);
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch;
|
||||
VerticalAlignment = VerticalAlignment.Stretch;
|
||||
Child = new Grid
|
||||
{
|
||||
RowDefinitions = new RowDefinitions("Auto,*"),
|
||||
RowSpacing = 14,
|
||||
Children =
|
||||
{
|
||||
new StackPanel
|
||||
{
|
||||
Spacing = 4,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
Children =
|
||||
{
|
||||
_timeTextBlock,
|
||||
_subtitleTextBlock
|
||||
}
|
||||
},
|
||||
_statusHost
|
||||
}
|
||||
};
|
||||
|
||||
Grid.SetRow(((Grid)Child).Children[1], 1);
|
||||
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
|
||||
RefreshClock(_clockService.CurrentTime);
|
||||
UpdateSubtitle();
|
||||
RefreshStatusPanel();
|
||||
ApplyScale();
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_instanceId))
|
||||
{
|
||||
_instanceId = _stateService.RegisterComponentInstance(
|
||||
_context.ComponentId,
|
||||
_context.PlacementId,
|
||||
_context.CellSize);
|
||||
}
|
||||
|
||||
_stateService.MarkFrontendReady(T(
|
||||
"status.frontend.detail.widget_connected",
|
||||
"组件界面已接入插件服务与通信。"));
|
||||
SubscribeToPluginBus();
|
||||
|
||||
RefreshClock(_clockService.CurrentTime);
|
||||
UpdateSubtitle();
|
||||
RefreshStatusPanel();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
foreach (var subscription in _subscriptions)
|
||||
{
|
||||
subscription.Dispose();
|
||||
}
|
||||
|
||||
_subscriptions.Clear();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_instanceId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_stateService.UnregisterComponentInstance(_instanceId);
|
||||
_instanceId = null;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ApplyScale();
|
||||
RefreshStatusPanel();
|
||||
}
|
||||
|
||||
private void SubscribeToPluginBus()
|
||||
{
|
||||
if (_subscriptions.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_subscriptions.Add(_messageBus.Subscribe<SamplePluginClockTickMessage>(message =>
|
||||
Dispatcher.UIThread.Post(() => RefreshClock(message.CurrentTime))));
|
||||
|
||||
_subscriptions.Add(_messageBus.Subscribe<SamplePluginStateChangedMessage>(_ =>
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
UpdateSubtitle();
|
||||
RefreshStatusPanel();
|
||||
})));
|
||||
}
|
||||
|
||||
private void RefreshClock(DateTimeOffset currentTime)
|
||||
{
|
||||
_timeTextBlock.Text = currentTime.LocalDateTime.ToString("HH:mm:ss");
|
||||
}
|
||||
|
||||
private void UpdateSubtitle()
|
||||
{
|
||||
var snapshot = _stateService.GetSnapshot();
|
||||
_subtitleTextBlock.Text = string.IsNullOrWhiteSpace(_context.PlacementId)
|
||||
? Tf("widget.subtitle.preview", "预览界面 | 已放置:{0}", snapshot.PlacedCount)
|
||||
: Tf("widget.subtitle.placement", "位置 {0} | 已放置:{1}", _context.PlacementId!, snapshot.PlacedCount);
|
||||
}
|
||||
|
||||
private void RefreshStatusPanel()
|
||||
{
|
||||
_statusPanel.Children.Clear();
|
||||
|
||||
var snapshot = _stateService.GetSnapshot();
|
||||
var basis = GetLayoutBasis();
|
||||
var titleSize = Math.Clamp(basis * 0.068, 11, 16);
|
||||
var detailSize = Math.Clamp(basis * 0.052, 9, 13);
|
||||
|
||||
foreach (var entry in snapshot.StatusEntries)
|
||||
{
|
||||
var palette = GetPalette(entry.State);
|
||||
_statusPanel.Children.Add(new Border
|
||||
{
|
||||
Background = new SolidColorBrush(palette.Background),
|
||||
BorderBrush = new SolidColorBrush(palette.Border),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(10, 8),
|
||||
Child = new Grid
|
||||
{
|
||||
RowDefinitions = new RowDefinitions("Auto,Auto"),
|
||||
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"),
|
||||
ColumnSpacing = 8,
|
||||
Children =
|
||||
{
|
||||
new Border
|
||||
{
|
||||
Width = Math.Clamp(basis * 0.038, 8, 11),
|
||||
Height = Math.Clamp(basis * 0.038, 8, 11),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Background = new SolidColorBrush(palette.Dot),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = entry.Title,
|
||||
FontSize = titleSize,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = Brushes.White,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = entry.Summary,
|
||||
FontSize = detailSize,
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFD7F2FF")),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
TextAlignment = TextAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = entry.Detail,
|
||||
FontSize = detailSize,
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFD7F2FF")),
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var row = (Grid)((Border)_statusPanel.Children[^1]).Child!;
|
||||
Grid.SetColumn(row.Children[1], 1);
|
||||
Grid.SetColumn(row.Children[2], 2);
|
||||
Grid.SetColumnSpan(row.Children[3], 3);
|
||||
Grid.SetRow(row.Children[3], 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyScale()
|
||||
{
|
||||
var basis = GetLayoutBasis();
|
||||
Padding = new Thickness(Math.Clamp(basis * 0.09, 16, 26));
|
||||
CornerRadius = new CornerRadius(Math.Clamp(basis * 0.14, 20, 34));
|
||||
_timeTextBlock.FontSize = Math.Clamp(basis * 0.22, 30, 58);
|
||||
_subtitleTextBlock.FontSize = Math.Clamp(basis * 0.062, 11, 17);
|
||||
_statusHost.Padding = new Thickness(Math.Clamp(basis * 0.045, 10, 18));
|
||||
_statusHost.CornerRadius = new CornerRadius(Math.Clamp(basis * 0.09, 14, 22));
|
||||
_statusPanel.Spacing = Math.Clamp(basis * 0.024, 6, 10);
|
||||
}
|
||||
|
||||
private double GetLayoutBasis()
|
||||
{
|
||||
var width = Bounds.Width > 1 ? Bounds.Width : _context.CellSize * 4;
|
||||
var height = Bounds.Height > 1 ? Bounds.Height : _context.CellSize * 4;
|
||||
return Math.Max(_context.CellSize * 4, Math.Min(width, height));
|
||||
}
|
||||
|
||||
private static (Color Background, Color Border, Color Dot) GetPalette(SamplePluginHealthState state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
SamplePluginHealthState.Healthy => (
|
||||
Color.Parse("#1F0F766E"),
|
||||
Color.Parse("#4D5EEAD4"),
|
||||
Color.Parse("#5EEAD4")),
|
||||
SamplePluginHealthState.Faulted => (
|
||||
Color.Parse("#29B91C1C"),
|
||||
Color.Parse("#66F87171"),
|
||||
Color.Parse("#F87171")),
|
||||
_ => (
|
||||
Color.Parse("#1F7C2D12"),
|
||||
Color.Parse("#66FDBA74"),
|
||||
Color.Parse("#FDBA74"))
|
||||
};
|
||||
}
|
||||
|
||||
private string T(string key, string fallback)
|
||||
{
|
||||
return _localizer.GetString(key, fallback);
|
||||
}
|
||||
|
||||
private string Tf(string key, string fallback, params object[] args)
|
||||
{
|
||||
return _localizer.Format(key, fallback, args);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": "LanMountainDesktop.SamplePlugin",
|
||||
"name": "LanMountain Sample Plugin",
|
||||
"description": "Example plugin used to validate PluginSdk loading and isolation.",
|
||||
"author": "LanMountainDesktop",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": "1.0.0",
|
||||
"entranceAssembly": "LanMountainDesktop.SamplePlugin.dll"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
# 示例插件目录
|
||||
|
||||
## 中文
|
||||
|
||||
本目录用于存放阑山桌面的示例插件和参考实现。
|
||||
|
||||
当前标准示例为 `LanMountainDesktop.SamplePlugin`。
|
||||
|
||||
## English
|
||||
|
||||
This directory stores sample plugins and reference implementations. The current standard sample is `LanMountainDesktop.SamplePlugin`.
|
||||
@@ -1,9 +0,0 @@
|
||||
# 插件标准说明
|
||||
|
||||
## 中文
|
||||
|
||||
本目录存放插件开发需要遵循的基础约定,包括 `.laapp`、`plugin.json`、`Localization/` 以及仓库根目录 README 和安装包等要求。
|
||||
|
||||
## English
|
||||
|
||||
This directory stores the baseline conventions for plugin development, including `.laapp`, `plugin.json`, `Localization/`, and repository-root deliverables.
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": "LanMountainDesktop.YourPlugin",
|
||||
"name": "Your Plugin",
|
||||
"description": "Describe what your plugin adds to LanMountainDesktop.",
|
||||
"author": "Your Name",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": "1.0.0",
|
||||
"entranceAssembly": "LanMountainDesktop.YourPlugin.dll"
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
using System.IO.Compression;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
return await RunAsync(args);
|
||||
|
||||
static async Task<int> RunAsync(string[] args)
|
||||
{
|
||||
if (args.Length == 0 || args.Any(arg => string.Equals(arg, "--help", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
PrintUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
string? inputDirectory = null;
|
||||
string? outputPath = null;
|
||||
var overwrite = false;
|
||||
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
switch (args[i])
|
||||
{
|
||||
case "--input":
|
||||
inputDirectory = ReadValue(args, ref i, "--input");
|
||||
break;
|
||||
case "--output":
|
||||
outputPath = ReadValue(args, ref i, "--output");
|
||||
break;
|
||||
case "--overwrite":
|
||||
overwrite = true;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown argument '{args[i]}'.");
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inputDirectory))
|
||||
{
|
||||
throw new InvalidOperationException("Missing required argument '--input'.");
|
||||
}
|
||||
|
||||
var fullInputDirectory = Path.GetFullPath(inputDirectory);
|
||||
if (!Directory.Exists(fullInputDirectory))
|
||||
{
|
||||
throw new DirectoryNotFoundException($"Plugin build directory '{fullInputDirectory}' was not found.");
|
||||
}
|
||||
|
||||
var manifestPath = Path.Combine(fullInputDirectory, PluginSdkInfo.ManifestFileName);
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
$"Plugin build directory '{fullInputDirectory}' does not contain '{PluginSdkInfo.ManifestFileName}'.",
|
||||
manifestPath);
|
||||
}
|
||||
|
||||
var manifest = PluginManifest.Load(manifestPath);
|
||||
var entranceAssemblyPath = manifest.ResolveEntranceAssemblyPath(manifestPath);
|
||||
if (!File.Exists(entranceAssemblyPath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
$"The entrance assembly declared by '{PluginSdkInfo.ManifestFileName}' was not found.",
|
||||
entranceAssemblyPath);
|
||||
}
|
||||
|
||||
outputPath ??= Path.Combine(
|
||||
Path.GetDirectoryName(fullInputDirectory) ?? fullInputDirectory,
|
||||
BuildPackageFileName(manifest.Id));
|
||||
|
||||
var fullOutputPath = Path.GetFullPath(outputPath);
|
||||
var inputDirectoryWithSeparator = EnsureTrailingSeparator(fullInputDirectory);
|
||||
if (fullOutputPath.StartsWith(inputDirectoryWithSeparator, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException("The output .laapp path cannot be placed inside the source directory.");
|
||||
}
|
||||
|
||||
var destinationDirectory = Path.GetDirectoryName(fullOutputPath);
|
||||
if (string.IsNullOrWhiteSpace(destinationDirectory))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to determine the output directory for the .laapp package.");
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(destinationDirectory);
|
||||
if (File.Exists(fullOutputPath))
|
||||
{
|
||||
if (!overwrite)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The output package '{fullOutputPath}' already exists. Pass '--overwrite' to replace it.");
|
||||
}
|
||||
|
||||
File.Delete(fullOutputPath);
|
||||
}
|
||||
|
||||
await Task.Run(() => ZipFile.CreateFromDirectory(
|
||||
fullInputDirectory,
|
||||
fullOutputPath,
|
||||
CompressionLevel.Optimal,
|
||||
includeBaseDirectory: false));
|
||||
|
||||
Console.WriteLine($"Packaged '{manifest.Name}' to '{fullOutputPath}'.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static string ReadValue(IReadOnlyList<string> args, ref int index, string optionName)
|
||||
{
|
||||
var nextIndex = index + 1;
|
||||
if (nextIndex >= args.Count)
|
||||
{
|
||||
throw new InvalidOperationException($"Missing value for '{optionName}'.");
|
||||
}
|
||||
|
||||
index = nextIndex;
|
||||
return args[nextIndex];
|
||||
}
|
||||
|
||||
static string BuildPackageFileName(string pluginId)
|
||||
{
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
var safeName = new string(pluginId.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
|
||||
return safeName + PluginSdkInfo.PackageFileExtension;
|
||||
}
|
||||
|
||||
static string EnsureTrailingSeparator(string path)
|
||||
{
|
||||
return path.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)
|
||||
? path
|
||||
: path + Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
static void PrintUsage()
|
||||
{
|
||||
Console.WriteLine("LanMountainDesktop.PluginPackager");
|
||||
Console.WriteLine("Usage:");
|
||||
Console.WriteLine(" --input <plugin build directory> Required");
|
||||
Console.WriteLine(" --output <path to .laapp> Optional");
|
||||
Console.WriteLine(" --overwrite Optional");
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Avalonia;
|
||||
using LanMountainDesktop.Settings.Core;
|
||||
using LanMountainDesktop.Shared.Contracts;
|
||||
|
||||
namespace LanMountainDesktop.Appearance;
|
||||
|
||||
public static class AppearanceCornerRadiusTokenFactory
|
||||
{
|
||||
public static AppearanceCornerRadiusTokens Create(double scale)
|
||||
{
|
||||
var normalizedScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(scale);
|
||||
return new AppearanceCornerRadiusTokens(
|
||||
Radius(6, normalizedScale),
|
||||
Radius(10, normalizedScale),
|
||||
Radius(14, normalizedScale),
|
||||
Radius(18, normalizedScale),
|
||||
Radius(24, normalizedScale),
|
||||
Radius(30, normalizedScale),
|
||||
Radius(36, normalizedScale));
|
||||
}
|
||||
|
||||
private static CornerRadius Radius(double value, double scale)
|
||||
{
|
||||
var scaled = Math.Round(value * scale * 2, MidpointRounding.AwayFromZero) / 2d;
|
||||
return new CornerRadius(scaled);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Settings.Core\LanMountainDesktop.Settings.Core.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Host.Abstractions\LanMountainDesktop.Host.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Settings.Core\LanMountainDesktop.Settings.Core.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Appearance\LanMountainDesktop.Appearance.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Host.Abstractions\LanMountainDesktop.Host.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
31
LanMountainDesktop.DesktopHost/DesktopBootstrap.cs
Normal file
31
LanMountainDesktop.DesktopHost/DesktopBootstrap.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
|
||||
namespace LanMountainDesktop.DesktopHost;
|
||||
|
||||
public static class DesktopBootstrap
|
||||
{
|
||||
public static void InitializeStartupServices(
|
||||
Action initializeTelemetryIdentity,
|
||||
Action initializeCrashTelemetry,
|
||||
Action initializeUsageTelemetry,
|
||||
Action scheduleStartupCleanup)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(initializeTelemetryIdentity);
|
||||
ArgumentNullException.ThrowIfNull(initializeCrashTelemetry);
|
||||
ArgumentNullException.ThrowIfNull(initializeUsageTelemetry);
|
||||
ArgumentNullException.ThrowIfNull(scheduleStartupCleanup);
|
||||
|
||||
initializeTelemetryIdentity();
|
||||
initializeCrashTelemetry();
|
||||
initializeUsageTelemetry();
|
||||
scheduleStartupCleanup();
|
||||
}
|
||||
|
||||
public static void InitializeApplication(Application application, Action initializeShell)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(application);
|
||||
ArgumentNullException.ThrowIfNull(initializeShell);
|
||||
initializeShell();
|
||||
}
|
||||
}
|
||||
55
LanMountainDesktop.DesktopHost/DesktopShellHost.cs
Normal file
55
LanMountainDesktop.DesktopHost/DesktopShellHost.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using LanMountainDesktop.Host.Abstractions;
|
||||
|
||||
namespace LanMountainDesktop.DesktopHost;
|
||||
|
||||
public sealed class DesktopShellHost : IDesktopShellHost
|
||||
{
|
||||
private readonly Action _initializePluginRuntime;
|
||||
private readonly Action _initializeTrayIcon;
|
||||
private readonly Action<IClassicDesktopStyleApplicationLifetime> _createAndAssignMainWindow;
|
||||
private readonly Action _performExitCleanup;
|
||||
private readonly Action _startActivationListener;
|
||||
private readonly Action _startWeatherRefresh;
|
||||
|
||||
public DesktopShellHost(
|
||||
Action initializePluginRuntime,
|
||||
Action initializeTrayIcon,
|
||||
Action<IClassicDesktopStyleApplicationLifetime> createAndAssignMainWindow,
|
||||
Action performExitCleanup,
|
||||
Action startActivationListener,
|
||||
Action startWeatherRefresh)
|
||||
{
|
||||
_initializePluginRuntime = initializePluginRuntime;
|
||||
_initializeTrayIcon = initializeTrayIcon;
|
||||
_createAndAssignMainWindow = createAndAssignMainWindow;
|
||||
_performExitCleanup = performExitCleanup;
|
||||
_startActivationListener = startActivationListener;
|
||||
_startWeatherRefresh = startWeatherRefresh;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
throw new InvalidOperationException("An application instance is required to initialize the desktop shell.");
|
||||
}
|
||||
|
||||
public void Initialize(Application application)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(application);
|
||||
|
||||
_initializePluginRuntime();
|
||||
_initializeTrayIcon();
|
||||
|
||||
if (application.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.Exit += (_, _) => _performExitCleanup();
|
||||
_createAndAssignMainWindow(desktop);
|
||||
_startActivationListener();
|
||||
}
|
||||
|
||||
_startWeatherRefresh();
|
||||
}
|
||||
}
|
||||
15
LanMountainDesktop.DesktopHost/DesktopStartupCoordinator.cs
Normal file
15
LanMountainDesktop.DesktopHost/DesktopStartupCoordinator.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace LanMountainDesktop.DesktopHost;
|
||||
|
||||
public sealed class DesktopStartupCoordinator
|
||||
{
|
||||
private readonly Action _restoreWorkspaceState;
|
||||
|
||||
public DesktopStartupCoordinator(Action restoreWorkspaceState)
|
||||
{
|
||||
_restoreWorkspaceState = restoreWorkspaceState ?? throw new ArgumentNullException(nameof(restoreWorkspaceState));
|
||||
}
|
||||
|
||||
public void Restore() => _restoreWorkspaceState();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Settings.Core\LanMountainDesktop.Settings.Core.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Appearance\LanMountainDesktop.Appearance.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.DesktopComponents.Runtime\LanMountainDesktop.DesktopComponents.Runtime.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Host.Abstractions\LanMountainDesktop.Host.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
18
LanMountainDesktop.DesktopHost/SettingsWindowHost.cs
Normal file
18
LanMountainDesktop.DesktopHost/SettingsWindowHost.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace LanMountainDesktop.DesktopHost;
|
||||
|
||||
public sealed class SettingsWindowHost
|
||||
{
|
||||
private readonly Action<string, string?> _openSettingsWindow;
|
||||
|
||||
public SettingsWindowHost(Action<string, string?> openSettingsWindow)
|
||||
{
|
||||
_openSettingsWindow = openSettingsWindow ?? throw new ArgumentNullException(nameof(openSettingsWindow));
|
||||
}
|
||||
|
||||
public void Open(string source, string? pageId = null)
|
||||
{
|
||||
_openSettingsWindow(source, pageId);
|
||||
}
|
||||
}
|
||||
19
LanMountainDesktop.DesktopHost/ShutdownCoordinator.cs
Normal file
19
LanMountainDesktop.DesktopHost/ShutdownCoordinator.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace LanMountainDesktop.DesktopHost;
|
||||
|
||||
public sealed class ShutdownCoordinator
|
||||
{
|
||||
private readonly Action<bool, string> _prepareForShutdown;
|
||||
private readonly Action<string> _resetShutdownIntent;
|
||||
|
||||
public ShutdownCoordinator(Action<bool, string> prepareForShutdown, Action<string> resetShutdownIntent)
|
||||
{
|
||||
_prepareForShutdown = prepareForShutdown ?? throw new ArgumentNullException(nameof(prepareForShutdown));
|
||||
_resetShutdownIntent = resetShutdownIntent ?? throw new ArgumentNullException(nameof(resetShutdownIntent));
|
||||
}
|
||||
|
||||
public void Prepare(bool isRestart, string source) => _prepareForShutdown(isRestart, source);
|
||||
|
||||
public void Reset(string source) => _resetShutdownIntent(source);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Shared.Contracts;
|
||||
|
||||
namespace LanMountainDesktop.Host.Abstractions;
|
||||
|
||||
public sealed record ComponentChromeContext(
|
||||
string ComponentId,
|
||||
string? PlacementId,
|
||||
double CellSize,
|
||||
double GlobalCornerRadiusScale,
|
||||
AppearanceCornerRadiusTokens CornerRadiusTokens,
|
||||
SettingsScope Scope = SettingsScope.App);
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace LanMountainDesktop.Host.Abstractions;
|
||||
|
||||
public interface IDesktopShellHost
|
||||
{
|
||||
void Initialize();
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>1.0.0</Version>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
10
LanMountainDesktop.PluginSdk/IPluginAppearanceContext.cs
Normal file
10
LanMountainDesktop.PluginSdk/IPluginAppearanceContext.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public interface IPluginAppearanceContext
|
||||
{
|
||||
PluginAppearanceSnapshot Snapshot { get; }
|
||||
|
||||
double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null);
|
||||
|
||||
double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null);
|
||||
}
|
||||
@@ -12,6 +12,8 @@ public interface IPluginRuntimeContext
|
||||
|
||||
IReadOnlyDictionary<string, object?> Properties { get; }
|
||||
|
||||
IPluginAppearanceContext Appearance { get; }
|
||||
|
||||
T? GetService<T>();
|
||||
|
||||
bool TryGetProperty<T>(string key, out T? value);
|
||||
|
||||
@@ -4,7 +4,15 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>3.0.0</Version>
|
||||
<Version>4.0.0</Version>
|
||||
<PackageId>LanMountainDesktop.PluginSdk</PackageId>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Authors>LanMountainDesktop</Authors>
|
||||
<Description>Official plugin SDK for LanMountainDesktop, including plugin manifest contracts, runtime interfaces, and registration extensions.</Description>
|
||||
<PackageTags>LanMountainDesktop;Plugin;SDK;Avalonia</PackageTags>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/wwiinnddyy/LanMountainDesktop</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -12,6 +20,13 @@
|
||||
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="buildTransitive\LanMountainDesktop.PluginSdk.props" Pack="true" PackagePath="buildTransitive\" />
|
||||
<None Include="buildTransitive\LanMountainDesktop.PluginSdk.targets" Pack="true" PackagePath="buildTransitive\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
49
LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs
Normal file
49
LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public sealed class PluginAppearanceContext : IPluginAppearanceContext
|
||||
{
|
||||
public PluginAppearanceContext(PluginAppearanceSnapshot snapshot)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
ArgumentNullException.ThrowIfNull(snapshot.CornerRadiusTokens);
|
||||
|
||||
Snapshot = snapshot with
|
||||
{
|
||||
GlobalCornerRadiusScale = Math.Max(0d, snapshot.GlobalCornerRadiusScale),
|
||||
ThemeVariant = string.IsNullOrWhiteSpace(snapshot.ThemeVariant)
|
||||
? "Unknown"
|
||||
: snapshot.ThemeVariant.Trim()
|
||||
};
|
||||
}
|
||||
|
||||
public PluginAppearanceSnapshot Snapshot { get; }
|
||||
|
||||
public double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null)
|
||||
{
|
||||
var scale = Snapshot.GlobalCornerRadiusScale;
|
||||
var scaled = Math.Max(0d, baseRadius) * scale;
|
||||
var scaledMin = minimum.HasValue ? minimum.Value * scale : scaled;
|
||||
var scaledMax = maximum.HasValue ? maximum.Value * scale : scaled;
|
||||
return minimum.HasValue || maximum.HasValue
|
||||
? Math.Clamp(scaled, scaledMin, scaledMax)
|
||||
: scaled;
|
||||
}
|
||||
|
||||
public double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null)
|
||||
{
|
||||
var resolved = Math.Max(0d, Snapshot.CornerRadiusTokens.Get(preset));
|
||||
if (!minimum.HasValue && !maximum.HasValue)
|
||||
{
|
||||
return resolved;
|
||||
}
|
||||
|
||||
var clampedMin = minimum ?? resolved;
|
||||
var clampedMax = maximum ?? resolved;
|
||||
if (clampedMin > clampedMax)
|
||||
{
|
||||
(clampedMin, clampedMax) = (clampedMax, clampedMin);
|
||||
}
|
||||
|
||||
return Math.Clamp(resolved, clampedMin, clampedMax);
|
||||
}
|
||||
}
|
||||
6
LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs
Normal file
6
LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public sealed record PluginAppearanceSnapshot(
|
||||
double GlobalCornerRadiusScale,
|
||||
PluginCornerRadiusTokens CornerRadiusTokens,
|
||||
string ThemeVariant);
|
||||
13
LanMountainDesktop.PluginSdk/PluginCornerRadiusPreset.cs
Normal file
13
LanMountainDesktop.PluginSdk/PluginCornerRadiusPreset.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public enum PluginCornerRadiusPreset
|
||||
{
|
||||
Default = 0,
|
||||
Micro = 1,
|
||||
Xs = 2,
|
||||
Sm = 3,
|
||||
Md = 4,
|
||||
Lg = 5,
|
||||
Xl = 6,
|
||||
Island = 7
|
||||
}
|
||||
49
LanMountainDesktop.PluginSdk/PluginCornerRadiusTokens.cs
Normal file
49
LanMountainDesktop.PluginSdk/PluginCornerRadiusTokens.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Avalonia;
|
||||
using LanMountainDesktop.Shared.Contracts;
|
||||
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public sealed record PluginCornerRadiusTokens(
|
||||
double Micro,
|
||||
double Xs,
|
||||
double Sm,
|
||||
double Md,
|
||||
double Lg,
|
||||
double Xl,
|
||||
double Island)
|
||||
{
|
||||
public double Get(PluginCornerRadiusPreset preset)
|
||||
{
|
||||
return preset switch
|
||||
{
|
||||
PluginCornerRadiusPreset.Default => Md,
|
||||
PluginCornerRadiusPreset.Micro => Micro,
|
||||
PluginCornerRadiusPreset.Xs => Xs,
|
||||
PluginCornerRadiusPreset.Sm => Sm,
|
||||
PluginCornerRadiusPreset.Md => Md,
|
||||
PluginCornerRadiusPreset.Lg => Lg,
|
||||
PluginCornerRadiusPreset.Xl => Xl,
|
||||
PluginCornerRadiusPreset.Island => Island,
|
||||
_ => Md
|
||||
};
|
||||
}
|
||||
|
||||
public CornerRadius ToCornerRadius(PluginCornerRadiusPreset preset)
|
||||
{
|
||||
return new CornerRadius(Get(preset));
|
||||
}
|
||||
|
||||
public static PluginCornerRadiusTokens FromShared(AppearanceCornerRadiusTokens tokens)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tokens);
|
||||
|
||||
return new PluginCornerRadiusTokens(
|
||||
tokens.Micro.TopLeft,
|
||||
tokens.Xs.TopLeft,
|
||||
tokens.Sm.TopLeft,
|
||||
tokens.Md.TopLeft,
|
||||
tokens.Lg.TopLeft,
|
||||
tokens.Xl.TopLeft,
|
||||
tokens.Island.TopLeft);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ public sealed class PluginDesktopComponentContext
|
||||
string componentId,
|
||||
string? placementId,
|
||||
double cellSize,
|
||||
IPluginAppearanceContext appearance,
|
||||
IPluginSettingsService? pluginSettings = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifest);
|
||||
@@ -19,6 +20,7 @@ public sealed class PluginDesktopComponentContext
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(componentId);
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(properties);
|
||||
ArgumentNullException.ThrowIfNull(appearance);
|
||||
|
||||
Manifest = manifest;
|
||||
PluginDirectory = pluginDirectory;
|
||||
@@ -28,6 +30,7 @@ public sealed class PluginDesktopComponentContext
|
||||
ComponentId = componentId.Trim();
|
||||
PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim();
|
||||
CellSize = Math.Max(1, cellSize);
|
||||
Appearance = appearance;
|
||||
PluginSettings = pluginSettings;
|
||||
}
|
||||
|
||||
@@ -47,8 +50,24 @@ public sealed class PluginDesktopComponentContext
|
||||
|
||||
public double CellSize { get; }
|
||||
|
||||
public IPluginAppearanceContext Appearance { get; }
|
||||
|
||||
public double GlobalCornerRadiusScale => Appearance.Snapshot.GlobalCornerRadiusScale;
|
||||
|
||||
public PluginCornerRadiusTokens CornerRadiusTokens => Appearance.Snapshot.CornerRadiusTokens;
|
||||
|
||||
public IPluginSettingsService? PluginSettings { get; }
|
||||
|
||||
public double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null)
|
||||
{
|
||||
return Appearance.ResolveScaledCornerRadius(baseRadius, minimum, maximum);
|
||||
}
|
||||
|
||||
public double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null)
|
||||
{
|
||||
return Appearance.ResolveCornerRadius(preset, minimum, maximum);
|
||||
}
|
||||
|
||||
public T? GetService<T>()
|
||||
{
|
||||
return (T?)Services.GetService(typeof(T));
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public sealed class PluginDesktopComponentOptions
|
||||
{
|
||||
public required string ComponentId { get; init; }
|
||||
|
||||
public required string DisplayName { get; init; }
|
||||
|
||||
public string IconKey { get; init; } = "PuzzlePiece";
|
||||
|
||||
public string Category { get; init; } = "Plugins";
|
||||
|
||||
public int MinWidthCells { get; init; } = 2;
|
||||
|
||||
public int MinHeightCells { get; init; } = 2;
|
||||
|
||||
public bool AllowDesktopPlacement { get; init; } = true;
|
||||
|
||||
public bool AllowStatusBarPlacement { get; init; }
|
||||
|
||||
public PluginDesktopComponentResizeMode ResizeMode { get; init; } = PluginDesktopComponentResizeMode.Proportional;
|
||||
|
||||
public string? DisplayNameLocalizationKey { get; init; }
|
||||
|
||||
public PluginCornerRadiusPreset CornerRadiusPreset { get; init; } = PluginCornerRadiusPreset.Default;
|
||||
|
||||
public Func<IPluginAppearanceContext, double, double>? CornerRadiusResolver { get; init; }
|
||||
}
|
||||
@@ -5,67 +5,37 @@ namespace LanMountainDesktop.PluginSdk;
|
||||
public sealed class PluginDesktopComponentRegistration
|
||||
{
|
||||
public PluginDesktopComponentRegistration(
|
||||
string componentId,
|
||||
string displayName,
|
||||
Func<IServiceProvider, PluginDesktopComponentContext, Control> controlFactory,
|
||||
string iconKey = "PuzzlePiece",
|
||||
string category = "Plugins",
|
||||
int minWidthCells = 2,
|
||||
int minHeightCells = 2,
|
||||
bool allowDesktopPlacement = true,
|
||||
bool allowStatusBarPlacement = false,
|
||||
PluginDesktopComponentResizeMode resizeMode = PluginDesktopComponentResizeMode.Proportional,
|
||||
string? displayNameLocalizationKey = null,
|
||||
Func<double, double>? cornerRadiusResolver = null)
|
||||
PluginDesktopComponentOptions options)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(componentId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(displayName);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(iconKey);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(category);
|
||||
ArgumentNullException.ThrowIfNull(controlFactory);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(options.ComponentId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(options.DisplayName);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(options.IconKey);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(options.Category);
|
||||
|
||||
ComponentId = componentId.Trim();
|
||||
DisplayName = displayName.Trim();
|
||||
DisplayNameLocalizationKey = string.IsNullOrWhiteSpace(displayNameLocalizationKey)
|
||||
ComponentId = options.ComponentId.Trim();
|
||||
DisplayName = options.DisplayName.Trim();
|
||||
DisplayNameLocalizationKey = string.IsNullOrWhiteSpace(options.DisplayNameLocalizationKey)
|
||||
? null
|
||||
: displayNameLocalizationKey.Trim();
|
||||
: options.DisplayNameLocalizationKey.Trim();
|
||||
ControlFactory = controlFactory;
|
||||
IconKey = iconKey.Trim();
|
||||
Category = category.Trim();
|
||||
MinWidthCells = Math.Max(1, minWidthCells);
|
||||
MinHeightCells = Math.Max(1, minHeightCells);
|
||||
AllowDesktopPlacement = allowDesktopPlacement;
|
||||
AllowStatusBarPlacement = allowStatusBarPlacement;
|
||||
ResizeMode = resizeMode;
|
||||
CornerRadiusResolver = cornerRadiusResolver;
|
||||
IconKey = options.IconKey.Trim();
|
||||
Category = options.Category.Trim();
|
||||
MinWidthCells = Math.Max(1, options.MinWidthCells);
|
||||
MinHeightCells = Math.Max(1, options.MinHeightCells);
|
||||
AllowDesktopPlacement = options.AllowDesktopPlacement;
|
||||
AllowStatusBarPlacement = options.AllowStatusBarPlacement;
|
||||
ResizeMode = options.ResizeMode;
|
||||
CornerRadiusPreset = options.CornerRadiusPreset;
|
||||
CornerRadiusResolver = options.CornerRadiusResolver;
|
||||
}
|
||||
|
||||
public PluginDesktopComponentRegistration(
|
||||
string componentId,
|
||||
string displayName,
|
||||
Func<PluginDesktopComponentContext, Control> controlFactory,
|
||||
string iconKey = "PuzzlePiece",
|
||||
string category = "Plugins",
|
||||
int minWidthCells = 2,
|
||||
int minHeightCells = 2,
|
||||
bool allowDesktopPlacement = true,
|
||||
bool allowStatusBarPlacement = false,
|
||||
PluginDesktopComponentResizeMode resizeMode = PluginDesktopComponentResizeMode.Proportional,
|
||||
string? displayNameLocalizationKey = null,
|
||||
Func<double, double>? cornerRadiusResolver = null)
|
||||
: this(
|
||||
componentId,
|
||||
displayName,
|
||||
(_, context) => controlFactory(context),
|
||||
iconKey,
|
||||
category,
|
||||
minWidthCells,
|
||||
minHeightCells,
|
||||
allowDesktopPlacement,
|
||||
allowStatusBarPlacement,
|
||||
resizeMode,
|
||||
displayNameLocalizationKey,
|
||||
cornerRadiusResolver)
|
||||
PluginDesktopComponentOptions options)
|
||||
: this((_, context) => controlFactory(context), options)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -91,5 +61,25 @@ public sealed class PluginDesktopComponentRegistration
|
||||
|
||||
public PluginDesktopComponentResizeMode ResizeMode { get; }
|
||||
|
||||
public Func<double, double>? CornerRadiusResolver { get; }
|
||||
public PluginCornerRadiusPreset CornerRadiusPreset { get; }
|
||||
|
||||
public Func<IPluginAppearanceContext, double, double>? CornerRadiusResolver { get; }
|
||||
|
||||
public double ResolveCornerRadius(IPluginAppearanceContext appearance, double cellSize)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(appearance);
|
||||
|
||||
var resolved = CornerRadiusResolver is not null
|
||||
? CornerRadiusResolver(appearance, Math.Max(1d, cellSize))
|
||||
: CornerRadiusPreset == PluginCornerRadiusPreset.Default
|
||||
? appearance.ResolveScaledCornerRadius(
|
||||
Math.Clamp(Math.Max(1d, cellSize) * 0.22, 8, 18),
|
||||
8,
|
||||
18)
|
||||
: appearance.ResolveCornerRadius(CornerRadiusPreset);
|
||||
|
||||
return double.IsFinite(resolved)
|
||||
? Math.Max(0d, resolved)
|
||||
: appearance.ResolveCornerRadius(PluginCornerRadiusPreset.Default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ public sealed record PluginManifest(
|
||||
throw new InvalidOperationException(
|
||||
$"Plugin '{normalized.Id}' targets API version '{normalized.ApiVersion}' (major {requestedVersion.Major}), " +
|
||||
$"but the host provides '{PluginSdkInfo.ApiVersion}' (major {currentVersion.Major}). " +
|
||||
$"This host only supports v{currentVersion.Major}.x plugins. " +
|
||||
$"Migrate the plugin to API {PluginSdkInfo.ApiVersion} and rebuild the package.");
|
||||
$"This host only supports v{currentVersion.Major}.x plugins and rejects v{requestedVersion.Major}.x packages by default. " +
|
||||
$"Migrate the plugin manifest and code to API {PluginSdkInfo.ApiVersion}, then rebuild and republish the package.");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
|
||||
@@ -2,7 +2,7 @@ namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public static class PluginSdkInfo
|
||||
{
|
||||
public const string ApiVersion = "3.0.0";
|
||||
public const string ApiVersion = "4.0.0";
|
||||
public const string ManifestFileName = "plugin.json";
|
||||
public const string PackageFileExtension = ".laapp";
|
||||
public const string DataDirectoryName = "Data";
|
||||
|
||||
@@ -30,34 +30,15 @@ public static class PluginServiceCollectionExtensions
|
||||
|
||||
public static IServiceCollection AddPluginDesktopComponent<TControl>(
|
||||
this IServiceCollection services,
|
||||
string componentId,
|
||||
string displayName,
|
||||
string iconKey = "PuzzlePiece",
|
||||
string category = "Plugins",
|
||||
int minWidthCells = 2,
|
||||
int minHeightCells = 2,
|
||||
bool allowDesktopPlacement = true,
|
||||
bool allowStatusBarPlacement = false,
|
||||
PluginDesktopComponentResizeMode resizeMode = PluginDesktopComponentResizeMode.Proportional,
|
||||
string? displayNameLocalizationKey = null,
|
||||
Func<double, double>? cornerRadiusResolver = null)
|
||||
PluginDesktopComponentOptions options)
|
||||
where TControl : Control
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
services.AddSingleton(new PluginDesktopComponentRegistration(
|
||||
componentId,
|
||||
displayName,
|
||||
(provider, context) => ActivatorUtilities.CreateInstance<TControl>(provider, context),
|
||||
iconKey,
|
||||
category,
|
||||
minWidthCells,
|
||||
minHeightCells,
|
||||
allowDesktopPlacement,
|
||||
allowStatusBarPlacement,
|
||||
resizeMode,
|
||||
displayNameLocalizationKey,
|
||||
cornerRadiusResolver));
|
||||
options));
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
21
LanMountainDesktop.PluginSdk/README.md
Normal file
21
LanMountainDesktop.PluginSdk/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# LanMountainDesktop.PluginSdk
|
||||
|
||||
Official SDK package for LanMountainDesktop plugins.
|
||||
|
||||
## Includes
|
||||
|
||||
- `IPlugin`/`PluginBase` entry abstractions
|
||||
- `PluginManifest` and shared contract declarations
|
||||
- desktop component registration extensions
|
||||
- plugin runtime context and host service abstractions
|
||||
- build-transitive packaging targets for `.laapp` output
|
||||
|
||||
## Quick Start
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LanMountainDesktop.PluginSdk" Version="4.0.0" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
Create `plugin.json` in your plugin project root, then run `dotnet build` to produce both build output and a `.laapp` package.
|
||||
@@ -6,7 +6,9 @@ public static class SettingsCategories
|
||||
public const string Appearance = "Appearance";
|
||||
public const string Components = "Components";
|
||||
public const string Plugins = "Plugins";
|
||||
public const string PluginMarket = "PluginMarket";
|
||||
public const string PluginCatalog = "PluginCatalog";
|
||||
[Obsolete("Use PluginCatalog instead.")]
|
||||
public const string PluginMarket = PluginCatalog;
|
||||
public const string Update = "Update";
|
||||
public const string About = "About";
|
||||
public const string Advanced = "Advanced";
|
||||
|
||||
@@ -6,6 +6,8 @@ public enum SettingsPageCategory
|
||||
Appearance = 10,
|
||||
Components = 20,
|
||||
Plugins = 30,
|
||||
PluginCatalog = 35,
|
||||
[Obsolete("Use PluginCatalog instead.")]
|
||||
PluginMarket = 35,
|
||||
About = 40
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<LanMountainPluginManifestFileName Condition="'$(LanMountainPluginManifestFileName)' == ''">plugin.json</LanMountainPluginManifestFileName>
|
||||
<LanMountainPluginPackageExtension Condition="'$(LanMountainPluginPackageExtension)' == ''">.laapp</LanMountainPluginPackageExtension>
|
||||
<LanMountainPluginPackageOutputDirectory Condition="'$(LanMountainPluginPackageOutputDirectory)' == ''">$(MSBuildProjectDirectory)\</LanMountainPluginPackageOutputDirectory>
|
||||
|
||||
<LanMountainPluginEnablePackaging Condition="'$(LanMountainPluginEnablePackaging)' == '' and Exists('$(MSBuildProjectDirectory)\$(LanMountainPluginManifestFileName)')">true</LanMountainPluginEnablePackaging>
|
||||
<LanMountainPluginEnablePackaging Condition="'$(LanMountainPluginEnablePackaging)' == ''">false</LanMountainPluginEnablePackaging>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="Exists('$(MSBuildProjectDirectory)\$(LanMountainPluginManifestFileName)')">
|
||||
<None Update="$(LanMountainPluginManifestFileName)" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,51 @@
|
||||
<Project>
|
||||
<Target Name="ValidateLanMountainPluginManifest"
|
||||
BeforeTargets="Build"
|
||||
Condition="'$(LanMountainPluginEnablePackaging)' == 'true'">
|
||||
<Error Condition="!Exists('$(MSBuildProjectDirectory)\$(LanMountainPluginManifestFileName)')"
|
||||
Text="LanMountain plugin packaging is enabled, but '$(LanMountainPluginManifestFileName)' was not found in '$(MSBuildProjectDirectory)'." />
|
||||
</Target>
|
||||
|
||||
<Target Name="CreateLanMountainPluginPackage"
|
||||
AfterTargets="Build"
|
||||
Condition="'$(LanMountainPluginEnablePackaging)' == 'true'">
|
||||
<PropertyGroup>
|
||||
<_LanMountainPluginBuildOutputDirectory>$(LanMountainPluginBuildOutputDirectory)</_LanMountainPluginBuildOutputDirectory>
|
||||
<_LanMountainPluginBuildOutputDirectory Condition="'$(_LanMountainPluginBuildOutputDirectory)' == ''">$(TargetDir)</_LanMountainPluginBuildOutputDirectory>
|
||||
<_LanMountainPluginBuildOutputDirectory Condition="'$(_LanMountainPluginBuildOutputDirectory)' == ''">$(MSBuildProjectDirectory)\$(OutputPath)</_LanMountainPluginBuildOutputDirectory>
|
||||
<_LanMountainPluginAssemblyName>$(LanMountainPluginAssemblyName)</_LanMountainPluginAssemblyName>
|
||||
<_LanMountainPluginAssemblyName Condition="'$(_LanMountainPluginAssemblyName)' == '' and '$(AssemblyName)' != ''">$(AssemblyName)</_LanMountainPluginAssemblyName>
|
||||
<_LanMountainPluginAssemblyName Condition="'$(_LanMountainPluginAssemblyName)' == ''">$(MSBuildProjectName)</_LanMountainPluginAssemblyName>
|
||||
<_LanMountainPluginPackageVersion>$(LanMountainPluginPackageVersion)</_LanMountainPluginPackageVersion>
|
||||
<_LanMountainPluginPackageVersion Condition="'$(_LanMountainPluginPackageVersion)' == '' and '$(Version)' != ''">$(Version)</_LanMountainPluginPackageVersion>
|
||||
<_LanMountainPluginPackageVersion Condition="'$(_LanMountainPluginPackageVersion)' == ''">1.0.0</_LanMountainPluginPackageVersion>
|
||||
<_LanMountainPluginPackageOutputDirectory>$(LanMountainPluginPackageOutputDirectory)</_LanMountainPluginPackageOutputDirectory>
|
||||
<_LanMountainPluginPackageOutputDirectory Condition="'$(_LanMountainPluginPackageOutputDirectory)' == ''">$(MSBuildProjectDirectory)\</_LanMountainPluginPackageOutputDirectory>
|
||||
<_LanMountainPluginPackageFileName>$(LanMountainPluginPackageFileName)</_LanMountainPluginPackageFileName>
|
||||
<_LanMountainPluginPackageFileName Condition="'$(_LanMountainPluginPackageFileName)' == ''">$(_LanMountainPluginAssemblyName).$(_LanMountainPluginPackageVersion)$(LanMountainPluginPackageExtension)</_LanMountainPluginPackageFileName>
|
||||
<_LanMountainPluginPackagePath>$(LanMountainPluginPackagePath)</_LanMountainPluginPackagePath>
|
||||
<_LanMountainPluginPackagePath Condition="'$(_LanMountainPluginPackagePath)' == ''">$(_LanMountainPluginPackageOutputDirectory)$(_LanMountainPluginPackageFileName)</_LanMountainPluginPackagePath>
|
||||
<_LanMountainPluginManifestOutputPath>$(_LanMountainPluginBuildOutputDirectory)$(LanMountainPluginManifestFileName)</_LanMountainPluginManifestOutputPath>
|
||||
<_LanMountainPluginDepsPath>$(ProjectDepsFilePath)</_LanMountainPluginDepsPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<Copy SourceFiles="$(MSBuildProjectDirectory)\$(LanMountainPluginManifestFileName)"
|
||||
DestinationFiles="$(_LanMountainPluginManifestOutputPath)"
|
||||
SkipUnchangedFiles="true"
|
||||
Condition="Exists('$(MSBuildProjectDirectory)\$(LanMountainPluginManifestFileName)')" />
|
||||
|
||||
<Error Condition="!Exists('$(_LanMountainPluginManifestOutputPath)')"
|
||||
Text="Plugin manifest '$(_LanMountainPluginManifestOutputPath)' was not found in build output. Ensure '$(LanMountainPluginManifestFileName)' is copied to output." />
|
||||
<Error Condition="!Exists('$(TargetPath)')"
|
||||
Text="Plugin assembly '$(TargetPath)' was not found. Build output is incomplete." />
|
||||
<Error Condition="'$(_LanMountainPluginDepsPath)' != '' and !Exists('$(_LanMountainPluginDepsPath)')"
|
||||
Text="Plugin deps file '$(_LanMountainPluginDepsPath)' was not found. Plugin packages must include a .deps.json file." />
|
||||
|
||||
<MakeDir Directories="$(_LanMountainPluginPackageOutputDirectory)" />
|
||||
<Delete Files="$(_LanMountainPluginPackagePath)" TreatErrorsAsWarnings="true" />
|
||||
<ZipDirectory SourceDirectory="$(_LanMountainPluginBuildOutputDirectory)"
|
||||
DestinationFile="$(_LanMountainPluginPackagePath)" />
|
||||
<Message Importance="High"
|
||||
Text="LanMountain plugin package generated: $(_LanMountainPluginPackagePath)" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<NoWarn>$(NoWarn);NU5128</NoWarn>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<PackageId>LanMountainDesktop.PluginTemplate</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>LanMountainDesktop</Authors>
|
||||
<Description>Official dotnet new template package for LanMountainDesktop plugins.</Description>
|
||||
<PackageTags>LanMountainDesktop;Plugin;Template;dotnet-new</PackageTags>
|
||||
<PackageType>Template</PackageType>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/wwiinnddyy/LanMountainDesktop</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<IsPackable>true</IsPackable>
|
||||
<NoDefaultExcludes>true</NoDefaultExcludes>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="content\**\*.cs" />
|
||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="content\**\*" Pack="true" PackagePath="content\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
17
LanMountainDesktop.PluginTemplate/README.md
Normal file
17
LanMountainDesktop.PluginTemplate/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# LanMountainDesktop.PluginTemplate
|
||||
|
||||
Official `dotnet new` template package for LanMountainDesktop plugins.
|
||||
|
||||
## Install
|
||||
|
||||
```powershell
|
||||
dotnet new install LanMountainDesktop.PluginTemplate
|
||||
```
|
||||
|
||||
## Create a plugin
|
||||
|
||||
```powershell
|
||||
dotnet new lmd-plugin -n YourPluginName
|
||||
```
|
||||
|
||||
The generated project references `LanMountainDesktop.PluginSdk` and produces a `.laapp` package automatically when built.
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/template",
|
||||
"author": "LanMountainDesktop",
|
||||
"classifications": [
|
||||
"LanMountainDesktop",
|
||||
"Plugin",
|
||||
"Desktop"
|
||||
],
|
||||
"name": "LanMountainDesktop Plugin",
|
||||
"identity": "LanMountainDesktop.PluginTemplate.CSharp",
|
||||
"shortName": "lmd-plugin",
|
||||
"sourceName": "LanMountainDesktop.PluginTemplate",
|
||||
"preferNameDirectory": true,
|
||||
"tags": {
|
||||
"type": "project",
|
||||
"language": "C#"
|
||||
},
|
||||
"symbols": {
|
||||
"pluginId": {
|
||||
"type": "parameter",
|
||||
"datatype": "text",
|
||||
"defaultValue": "LanMountainDesktop.PluginTemplate",
|
||||
"description": "Plugin manifest id.",
|
||||
"replaces": "__PLUGIN_ID__"
|
||||
},
|
||||
"pluginAuthor": {
|
||||
"type": "parameter",
|
||||
"datatype": "text",
|
||||
"defaultValue": "Your Name",
|
||||
"description": "Plugin author.",
|
||||
"replaces": "__PLUGIN_AUTHOR__"
|
||||
},
|
||||
"pluginName": {
|
||||
"type": "parameter",
|
||||
"datatype": "text",
|
||||
"defaultValue": "LanMountain Plugin Template",
|
||||
"description": "Display name shown in plugin manifest.",
|
||||
"replaces": "__PLUGIN_NAME__"
|
||||
},
|
||||
"pluginDescription": {
|
||||
"type": "parameter",
|
||||
"datatype": "text",
|
||||
"defaultValue": "Plugin generated from the official LanMountainDesktop template.",
|
||||
"description": "Plugin description shown in plugin manifest.",
|
||||
"replaces": "__PLUGIN_DESCRIPTION__"
|
||||
},
|
||||
"pluginSdkVersion": {
|
||||
"type": "parameter",
|
||||
"datatype": "text",
|
||||
"defaultValue": "4.0.0",
|
||||
"description": "LanMountainDesktop.PluginSdk package version.",
|
||||
"replaces": "__PLUGIN_SDK_VERSION__"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>1.0.0</Version>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<LanMountainPluginPackageVersion>$(Version)</LanMountainPluginPackageVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LanMountainDesktop.PluginSdk" Version="__PLUGIN_SDK_VERSION__" ExcludeAssets="runtime" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="plugin.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="Localization\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
15
LanMountainDesktop.PluginTemplate/content/Plugin.cs
Normal file
15
LanMountainDesktop.PluginTemplate/content/Plugin.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace LanMountainDesktop.PluginTemplate;
|
||||
|
||||
[PluginEntrance]
|
||||
public sealed class Plugin : PluginBase
|
||||
{
|
||||
public override void Initialize(HostBuilderContext context, IServiceCollection services)
|
||||
{
|
||||
_ = context;
|
||||
_ = services;
|
||||
}
|
||||
}
|
||||
24
LanMountainDesktop.PluginTemplate/content/README.md
Normal file
24
LanMountainDesktop.PluginTemplate/content/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# __PLUGIN_NAME__
|
||||
|
||||
Official-style plugin scaffold generated for LanMountainDesktop.
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
dotnet build -c Release
|
||||
```
|
||||
|
||||
`LanMountainDesktop.PluginSdk` build targets will generate:
|
||||
|
||||
- plugin output files under `bin/<Configuration>/<TFM>/`
|
||||
- a `.laapp` package in the project root
|
||||
|
||||
## Manifest
|
||||
|
||||
Update `plugin.json` fields as needed before release:
|
||||
|
||||
- `id`
|
||||
- `name`
|
||||
- `description`
|
||||
- `author`
|
||||
- `version`
|
||||
10
LanMountainDesktop.PluginTemplate/content/plugin.json
Normal file
10
LanMountainDesktop.PluginTemplate/content/plugin.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "__PLUGIN_ID__",
|
||||
"name": "__PLUGIN_NAME__",
|
||||
"description": "__PLUGIN_DESCRIPTION__",
|
||||
"author": "__PLUGIN_AUTHOR__",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": "4.0.0",
|
||||
"entranceAssembly": "LanMountainDesktop.PluginTemplate.dll",
|
||||
"sharedContracts": []
|
||||
}
|
||||
18
LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs
Normal file
18
LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace LanMountainDesktop.Settings.Core;
|
||||
|
||||
public static class GlobalAppearanceSettings
|
||||
{
|
||||
public const double DefaultCornerRadiusScale = 1.0;
|
||||
public const double MinimumCornerRadiusScale = 0.0;
|
||||
public const double MaximumCornerRadiusScale = 2.50;
|
||||
|
||||
public static double NormalizeCornerRadiusScale(double value)
|
||||
{
|
||||
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||
{
|
||||
return DefaultCornerRadiusScale;
|
||||
}
|
||||
|
||||
return Math.Clamp(value, MinimumCornerRadiusScale, MaximumCornerRadiusScale);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>1.0.0</Version>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,12 @@
|
||||
using Avalonia;
|
||||
|
||||
namespace LanMountainDesktop.Shared.Contracts;
|
||||
|
||||
public sealed record AppearanceCornerRadiusTokens(
|
||||
CornerRadius Micro,
|
||||
CornerRadius Xs,
|
||||
CornerRadius Sm,
|
||||
CornerRadius Md,
|
||||
CornerRadius Lg,
|
||||
CornerRadius Xl,
|
||||
CornerRadius Island);
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Version>1.0.0</Version>
|
||||
<PackageId>LanMountainDesktop.Shared.Contracts</PackageId>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Authors>LanMountainDesktop</Authors>
|
||||
<Description>Shared contracts used by LanMountainDesktop host and plugins for cross-boundary communication.</Description>
|
||||
<PackageTags>LanMountainDesktop;Plugin;SharedContracts;Avalonia</PackageTags>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/wwiinnddyy/LanMountainDesktop</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
16
LanMountainDesktop.Shared.Contracts/README.md
Normal file
16
LanMountainDesktop.Shared.Contracts/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# LanMountainDesktop.Shared.Contracts
|
||||
|
||||
Shared contracts package for LanMountainDesktop host and plugin ecosystems.
|
||||
|
||||
## Includes
|
||||
|
||||
- cross-boundary records used by host/runtime and plugins
|
||||
- contract types intended for stable shared communication
|
||||
|
||||
## Usage
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LanMountainDesktop.Shared.Contracts" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using LanMountainDesktop.Appearance;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Host.Abstractions;
|
||||
using LanMountainDesktop.Views.Components;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class BuiltInDesktopHostCornerRadiusBaselineTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(80d, 0d)]
|
||||
[InlineData(120d, 1d)]
|
||||
[InlineData(160d, 2.5d)]
|
||||
public void BuiltInDesktopHosts_ResolveToTheUnifiedLgBaseline(double cellSize, double globalScale)
|
||||
{
|
||||
var registry = new DesktopComponentRuntimeRegistry(
|
||||
ComponentRegistry.CreateDefault(),
|
||||
DesktopComponentRuntimeRegistry.GetDefaultRegistrations());
|
||||
var expected = AppearanceCornerRadiusTokenFactory.Create(globalScale).Lg.TopLeft;
|
||||
|
||||
foreach (var descriptor in registry.GetDesktopComponents())
|
||||
{
|
||||
var resolved = descriptor.ResolveCornerRadius(CreateChromeContext(descriptor.Definition.Id, cellSize, globalScale));
|
||||
Assert.Equal(expected, resolved, 3);
|
||||
}
|
||||
}
|
||||
|
||||
private static ComponentChromeContext CreateChromeContext(
|
||||
string componentId,
|
||||
double cellSize,
|
||||
double globalScale)
|
||||
{
|
||||
return new ComponentChromeContext(
|
||||
componentId,
|
||||
null,
|
||||
cellSize,
|
||||
globalScale,
|
||||
AppearanceCornerRadiusTokenFactory.Create(globalScale));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Avalonia;
|
||||
using LanMountainDesktop.DesktopEditing;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class ComponentLibraryCollapseStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void CreateExpanded_InitializesExpandedStateAndHidesChip()
|
||||
{
|
||||
var margin = new Thickness(24, 24, 24, 100);
|
||||
var state = ComponentLibraryCollapseState.CreateExpanded(margin, 0.75);
|
||||
|
||||
Assert.Equal(ComponentLibraryCollapseVisualState.Expanded, state.VisualState);
|
||||
Assert.Equal(margin, state.ExpandedMargin);
|
||||
Assert.Equal(0.75, state.ExpandedOpacity, 3);
|
||||
Assert.False(state.IsChipVisible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithVisualState_PreservesStableExpandedSnapshotAcrossTransitions()
|
||||
{
|
||||
var margin = new Thickness(20, 18, 20, 96);
|
||||
var expanded = ComponentLibraryCollapseState.CreateExpanded(margin, 1);
|
||||
|
||||
var collapsing = expanded.WithVisualState(ComponentLibraryCollapseVisualState.Collapsing, isChipVisible: true);
|
||||
var collapsed = collapsing.WithVisualState(ComponentLibraryCollapseVisualState.Collapsed, isChipVisible: true);
|
||||
var restoring = collapsed.WithVisualState(ComponentLibraryCollapseVisualState.Restoring, isChipVisible: false);
|
||||
|
||||
Assert.Equal(ComponentLibraryCollapseVisualState.Collapsing, collapsing.VisualState);
|
||||
Assert.Equal(ComponentLibraryCollapseVisualState.Collapsed, collapsed.VisualState);
|
||||
Assert.Equal(ComponentLibraryCollapseVisualState.Restoring, restoring.VisualState);
|
||||
|
||||
Assert.Equal(margin, collapsing.ExpandedMargin);
|
||||
Assert.Equal(margin, collapsed.ExpandedMargin);
|
||||
Assert.Equal(margin, restoring.ExpandedMargin);
|
||||
|
||||
Assert.Equal(1, collapsing.ExpandedOpacity, 3);
|
||||
Assert.Equal(1, collapsed.ExpandedOpacity, 3);
|
||||
Assert.Equal(1, restoring.ExpandedOpacity, 3);
|
||||
|
||||
Assert.True(collapsing.IsChipVisible);
|
||||
Assert.True(collapsed.IsChipVisible);
|
||||
Assert.False(restoring.IsChipVisible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateExpanded_ProducesRestorableSnapshotEvenWhenOriginalOpacityIsLow()
|
||||
{
|
||||
var margin = new Thickness(18, 22, 18, 88);
|
||||
var expanded = ComponentLibraryCollapseState.CreateExpanded(margin, 0.15);
|
||||
var restored = expanded.WithVisualState(ComponentLibraryCollapseVisualState.Expanded, isChipVisible: false);
|
||||
|
||||
Assert.Equal(margin, restored.ExpandedMargin);
|
||||
Assert.Equal(0.15, restored.ExpandedOpacity, 3);
|
||||
Assert.Equal(ComponentLibraryCollapseVisualState.Expanded, restored.VisualState);
|
||||
Assert.False(restored.IsChipVisible);
|
||||
}
|
||||
}
|
||||
257
LanMountainDesktop.Tests/ComponentPreviewImageServiceTests.cs
Normal file
257
LanMountainDesktop.Tests/ComponentPreviewImageServiceTests.cs
Normal file
@@ -0,0 +1,257 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
using LanMountainDesktop.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class ComponentPreviewImageServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task QueueGenerationAsync_ExecutesWorkSeriallyAcrossKeys()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
var executionOrder = new List<string>();
|
||||
var activeCount = 0;
|
||||
var maxActiveCount = 0;
|
||||
|
||||
Task<ComponentPreviewImageEntry> Queue(string componentTypeId)
|
||||
{
|
||||
var key = ComponentPreviewKey.ForComponentType(componentTypeId, widthCells: 2, heightCells: 2);
|
||||
return service.QueueGenerationAsync(
|
||||
key,
|
||||
visualSignature: $"sig:{componentTypeId}",
|
||||
async _ =>
|
||||
{
|
||||
var activeNow = Interlocked.Increment(ref activeCount);
|
||||
maxActiveCount = Math.Max(maxActiveCount, activeNow);
|
||||
lock (executionOrder)
|
||||
{
|
||||
executionOrder.Add(componentTypeId);
|
||||
}
|
||||
|
||||
await Task.Delay(40);
|
||||
Interlocked.Decrement(ref activeCount);
|
||||
return CreateImage();
|
||||
});
|
||||
}
|
||||
|
||||
var first = Queue("Clock");
|
||||
var second = Queue("Weather");
|
||||
var third = Queue("Calendar");
|
||||
|
||||
await Task.WhenAll(first, second, third);
|
||||
|
||||
Assert.Equal(1, maxActiveCount);
|
||||
Assert.Equal(["Clock", "Weather", "Calendar"], executionOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueueGenerationAsync_DeduplicatesConcurrentRequestsForSameKey()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
||||
var generationCount = 0;
|
||||
var bitmap = CreateImage();
|
||||
var completion = new TaskCompletionSource<IImage?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
Task<IImage?> Generation(CancellationToken _)
|
||||
{
|
||||
Interlocked.Increment(ref generationCount);
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
var first = service.QueueGenerationAsync(key, "clock-sig", Generation);
|
||||
var second = service.QueueGenerationAsync(key, "clock-sig", Generation);
|
||||
|
||||
Assert.Same(first, second);
|
||||
|
||||
completion.SetResult(bitmap);
|
||||
var entry = await first;
|
||||
|
||||
Assert.Equal(1, generationCount);
|
||||
Assert.Equal(ComponentPreviewImageState.Ready, entry.State);
|
||||
Assert.Same(bitmap, entry.Bitmap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Invalidate_ResetsSingleKeyToPending()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
||||
var image = CreateDisposableImage();
|
||||
var stored = service.Store(key, image, "clock-sig");
|
||||
var previousRevision = stored.Revision;
|
||||
|
||||
var result = service.Invalidate(key);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(ComponentPreviewImageState.Pending, stored.State);
|
||||
Assert.Null(stored.Bitmap);
|
||||
Assert.True(image.IsDisposed);
|
||||
Assert.True(stored.Revision > previousRevision);
|
||||
Assert.Equal("clock-sig", stored.VisualSignature);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemovePlacementPreviews_RemovesOnlyMatchingPlacementEntries()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
|
||||
var removedClock = ComponentPreviewKey.ForPlacementInstance("Clock", "desk-1", widthCells: 2, heightCells: 2);
|
||||
var removedWeather = ComponentPreviewKey.ForPlacementInstance("Weather", "desk-1", widthCells: 4, heightCells: 2);
|
||||
var keptPlacement = ComponentPreviewKey.ForPlacementInstance("Clock", "desk-2", widthCells: 2, heightCells: 2);
|
||||
var keptType = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
||||
var removedClockImage = CreateDisposableImage();
|
||||
var removedWeatherImage = CreateDisposableImage();
|
||||
var keptPlacementImage = CreateDisposableImage();
|
||||
var keptTypeImage = CreateDisposableImage();
|
||||
|
||||
service.Store(removedClock, removedClockImage, "sig-a");
|
||||
service.Store(removedWeather, removedWeatherImage, "sig-b");
|
||||
service.Store(keptPlacement, keptPlacementImage, "sig-c");
|
||||
service.Store(keptType, keptTypeImage, "sig-d");
|
||||
|
||||
var removedCount = service.RemovePlacementPreviews("desk-1");
|
||||
|
||||
Assert.Equal(2, removedCount);
|
||||
Assert.False(service.TryGetEntry(removedClock, out _));
|
||||
Assert.False(service.TryGetEntry(removedWeather, out _));
|
||||
Assert.True(service.TryGetEntry(keptPlacement, out _));
|
||||
Assert.True(service.TryGetEntry(keptType, out _));
|
||||
Assert.True(removedClockImage.IsDisposed);
|
||||
Assert.True(removedWeatherImage.IsDisposed);
|
||||
Assert.False(keptPlacementImage.IsDisposed);
|
||||
Assert.False(keptTypeImage.IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidateVisualSignature_InvalidatesEveryMatchingEntry()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
const string matchingSignature = "shared-sig";
|
||||
const string otherSignature = "other-sig";
|
||||
|
||||
var first = service.Store(
|
||||
ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2),
|
||||
CreateImage(),
|
||||
matchingSignature);
|
||||
var second = service.Store(
|
||||
ComponentPreviewKey.ForPlacementInstance("Clock", "desk-1", widthCells: 2, heightCells: 2),
|
||||
CreateImage(),
|
||||
matchingSignature);
|
||||
var third = service.Store(
|
||||
ComponentPreviewKey.ForComponentType("Weather", widthCells: 2, heightCells: 1),
|
||||
CreateImage(),
|
||||
otherSignature);
|
||||
|
||||
var invalidatedCount = service.InvalidateVisualSignature(matchingSignature);
|
||||
|
||||
Assert.Equal(2, invalidatedCount);
|
||||
Assert.Equal(ComponentPreviewImageState.Pending, first.State);
|
||||
Assert.Equal(ComponentPreviewImageState.Pending, second.State);
|
||||
Assert.Null(first.Bitmap);
|
||||
Assert.Null(second.Bitmap);
|
||||
Assert.Equal(ComponentPreviewImageState.Ready, third.State);
|
||||
Assert.NotNull(third.Bitmap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Store_ReplacingBitmap_DisposesPreviousBitmap_WhenInstanceChanges()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
||||
var first = CreateDisposableImage();
|
||||
var second = CreateDisposableImage();
|
||||
|
||||
service.Store(key, first, "sig-a");
|
||||
service.Store(key, second, "sig-b");
|
||||
|
||||
Assert.True(first.IsDisposed);
|
||||
Assert.False(second.IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Store_ReplacingBitmap_DoesNotDispose_WhenSameInstanceReused()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
||||
var image = CreateDisposableImage();
|
||||
|
||||
service.Store(key, image, "sig-a");
|
||||
service.Store(key, image, "sig-b");
|
||||
|
||||
Assert.False(image.IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StoreFailure_DisposesExistingBitmap()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
||||
var image = CreateDisposableImage();
|
||||
|
||||
service.Store(key, image, "sig-a");
|
||||
var entry = service.StoreFailure(key, "sig-a", "failed");
|
||||
|
||||
Assert.True(image.IsDisposed);
|
||||
Assert.Equal(ComponentPreviewImageState.Failed, entry.State);
|
||||
Assert.Null(entry.Bitmap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueueGenerationAsync_DisposesStaleGeneratedBitmap_WhenEntryWasInvalidated()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
||||
var completion = new TaskCompletionSource<IImage?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var stale = CreateDisposableImage();
|
||||
|
||||
var generationTask = service.QueueGenerationAsync(key, "sig-a", _ => completion.Task);
|
||||
_ = service.Invalidate(key);
|
||||
completion.SetResult(stale);
|
||||
var entry = await generationTask;
|
||||
|
||||
Assert.True(stale.IsDisposed);
|
||||
Assert.Equal(ComponentPreviewImageState.Pending, entry.State);
|
||||
Assert.Null(entry.Bitmap);
|
||||
}
|
||||
|
||||
private static IImage CreateImage() => new TestImage();
|
||||
private static DisposableTestImage CreateDisposableImage() => new();
|
||||
|
||||
private sealed class TestImage : IImage
|
||||
{
|
||||
public Size Size => new(1, 1);
|
||||
|
||||
public void Draw(DrawingContext context, Rect sourceRect, Rect destRect)
|
||||
{
|
||||
_ = context;
|
||||
_ = sourceRect;
|
||||
_ = destRect;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DisposableTestImage : IImage, IDisposable
|
||||
{
|
||||
public Size Size => new(1, 1);
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
IsDisposed = true;
|
||||
}
|
||||
|
||||
public void Draw(DrawingContext context, Rect sourceRect, Rect destRect)
|
||||
{
|
||||
_ = context;
|
||||
_ = sourceRect;
|
||||
_ = destRect;
|
||||
}
|
||||
}
|
||||
}
|
||||
91
LanMountainDesktop.Tests/CornerRadiusScaleTests.cs
Normal file
91
LanMountainDesktop.Tests/CornerRadiusScaleTests.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using Avalonia;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Settings.Core;
|
||||
using LanMountainDesktop.Shared.Contracts;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class CornerRadiusScaleTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(-1d, 0d)]
|
||||
[InlineData(0d, 0d)]
|
||||
[InlineData(0.33d, 0.33d)]
|
||||
[InlineData(1.234d, 1.234d)]
|
||||
[InlineData(2.5d, 2.5d)]
|
||||
[InlineData(3d, 2.5d)]
|
||||
public void NormalizeCornerRadiusScale_ClampsWithoutSnapping(double input, double expected)
|
||||
{
|
||||
Assert.Equal(expected, GlobalAppearanceSettings.NormalizeCornerRadiusScale(input), 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeCornerRadiusScale_UsesDefaultForInvalidValues()
|
||||
{
|
||||
Assert.Equal(
|
||||
GlobalAppearanceSettings.DefaultCornerRadiusScale,
|
||||
GlobalAppearanceSettings.NormalizeCornerRadiusScale(double.NaN),
|
||||
3);
|
||||
Assert.Equal(
|
||||
GlobalAppearanceSettings.DefaultCornerRadiusScale,
|
||||
GlobalAppearanceSettings.NormalizeCornerRadiusScale(double.PositiveInfinity),
|
||||
3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PluginDesktopComponentContext_AllowsZeroRadiusScaling()
|
||||
{
|
||||
var appearanceContext = new PluginAppearanceContext(new PluginAppearanceSnapshot(
|
||||
GlobalCornerRadiusScale: 0d,
|
||||
CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(new AppearanceCornerRadiusTokens(
|
||||
new CornerRadius(6),
|
||||
new CornerRadius(10),
|
||||
new CornerRadius(14),
|
||||
new CornerRadius(18),
|
||||
new CornerRadius(24),
|
||||
new CornerRadius(30),
|
||||
new CornerRadius(36))),
|
||||
ThemeVariant: "Unknown"));
|
||||
|
||||
var context = new PluginDesktopComponentContext(
|
||||
new PluginManifest("plugin.id", "Plugin Name", "plugin.dll"),
|
||||
"C:\\Plugins\\plugin.id",
|
||||
"C:\\Data\\plugin.id",
|
||||
new NullServiceProvider(),
|
||||
new Dictionary<string, object?>(),
|
||||
"component-1",
|
||||
null,
|
||||
96d,
|
||||
appearanceContext);
|
||||
|
||||
Assert.Equal(0d, context.GlobalCornerRadiusScale, 3);
|
||||
Assert.Equal(0d, context.ResolveScaledCornerRadius(12d), 3);
|
||||
Assert.Equal(0d, context.ResolveScaledCornerRadius(12d, 8d, 18d), 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PluginAppearanceContext_ResolveCornerRadius_DoesNotDoubleScalePresetTokens()
|
||||
{
|
||||
var context = new PluginAppearanceContext(new PluginAppearanceSnapshot(
|
||||
GlobalCornerRadiusScale: 2d,
|
||||
CornerRadiusTokens: new PluginCornerRadiusTokens(
|
||||
Micro: 12d,
|
||||
Xs: 20d,
|
||||
Sm: 28d,
|
||||
Md: 36d,
|
||||
Lg: 48d,
|
||||
Xl: 60d,
|
||||
Island: 72d),
|
||||
ThemeVariant: "Light"));
|
||||
|
||||
Assert.Equal(36d, context.ResolveCornerRadius(PluginCornerRadiusPreset.Md), 3);
|
||||
Assert.Equal(36d, context.ResolveCornerRadius(PluginCornerRadiusPreset.Md, maximum: 40d), 3);
|
||||
Assert.Equal(36d, context.ResolveScaledCornerRadius(18d), 3);
|
||||
}
|
||||
|
||||
private sealed class NullServiceProvider : IServiceProvider
|
||||
{
|
||||
public object? GetService(Type serviceType) => null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using LanMountainDesktop.Host.Abstractions;
|
||||
using LanMountainDesktop.Shared.Contracts;
|
||||
using LanMountainDesktop.Views.Components;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class DesktopComponentRuntimeRegistrationCornerRadiusTests
|
||||
{
|
||||
[Fact]
|
||||
public void LegacyCellSizeResolver_AppliesGlobalCornerRadiusScale()
|
||||
{
|
||||
var registration = new DesktopComponentRuntimeRegistration(
|
||||
componentId: "test.component",
|
||||
displayNameLocalizationKey: null,
|
||||
controlFactory: () => new Border(),
|
||||
cornerRadiusResolver: cellSize => Math.Clamp(cellSize * 0.30, 10, 40));
|
||||
|
||||
var resolver = Assert.IsType<Func<ComponentChromeContext, double>>(registration.CornerRadiusResolver);
|
||||
var resolved = resolver(CreateChromeContext(cellSize: 120, globalScale: 2.0));
|
||||
|
||||
Assert.Equal(72.0, resolved, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChromeContextResolver_IsNotDoubleScaledByRegistrationWrapper()
|
||||
{
|
||||
var registration = new DesktopComponentRuntimeRegistration(
|
||||
componentId: "test.component",
|
||||
displayNameLocalizationKey: null,
|
||||
controlFactory: _ => new Border(),
|
||||
cornerRadiusResolver: chromeContext => chromeContext.CellSize + chromeContext.GlobalCornerRadiusScale);
|
||||
|
||||
var resolver = Assert.IsType<Func<ComponentChromeContext, double>>(registration.CornerRadiusResolver);
|
||||
var resolved = resolver(CreateChromeContext(cellSize: 50, globalScale: 2.5));
|
||||
|
||||
Assert.Equal(52.5, resolved, 3);
|
||||
}
|
||||
|
||||
private static ComponentChromeContext CreateChromeContext(double cellSize, double globalScale)
|
||||
{
|
||||
return new ComponentChromeContext(
|
||||
ComponentId: "test.component",
|
||||
PlacementId: null,
|
||||
CellSize: cellSize,
|
||||
GlobalCornerRadiusScale: globalScale,
|
||||
CornerRadiusTokens: new AppearanceCornerRadiusTokens(
|
||||
new CornerRadius(6),
|
||||
new CornerRadius(10),
|
||||
new CornerRadius(14),
|
||||
new CornerRadius(18),
|
||||
new CornerRadius(24),
|
||||
new CornerRadius(30),
|
||||
new CornerRadius(36)));
|
||||
}
|
||||
}
|
||||
15
LanMountainDesktop.Tests/DesktopEditCommitMathTests.cs
Normal file
15
LanMountainDesktop.Tests/DesktopEditCommitMathTests.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using LanMountainDesktop.DesktopEditing;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class DesktopEditCommitMathTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsPendingCommitValid_ReturnsTrueOnlyForMatchingActiveVersion()
|
||||
{
|
||||
Assert.True(DesktopEditCommitMath.IsPendingCommitValid(isPending: true, scheduledVersion: 4, currentVersion: 4));
|
||||
Assert.False(DesktopEditCommitMath.IsPendingCommitValid(isPending: false, scheduledVersion: 4, currentVersion: 4));
|
||||
Assert.False(DesktopEditCommitMath.IsPendingCommitValid(isPending: true, scheduledVersion: 4, currentVersion: 5));
|
||||
}
|
||||
}
|
||||
173
LanMountainDesktop.Tests/DesktopPlacementMathTests.cs
Normal file
173
LanMountainDesktop.Tests/DesktopPlacementMathTests.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using Avalonia;
|
||||
using LanMountainDesktop.DesktopEditing;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class DesktopPlacementMathTests
|
||||
{
|
||||
[Fact]
|
||||
public void ComputeDragStartThreshold_UsesFloorAndCellScale()
|
||||
{
|
||||
Assert.Equal(10d, DesktopPlacementMath.ComputeDragStartThreshold(24));
|
||||
Assert.Equal(14.4d, DesktopPlacementMath.ComputeDragStartThreshold(80), 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasExceededThreshold_OnlyReturnsTrueAfterEnoughMovement()
|
||||
{
|
||||
var start = new Point(20, 20);
|
||||
|
||||
Assert.False(DesktopPlacementMath.HasExceededThreshold(start, new Point(27, 25), 10));
|
||||
Assert.True(DesktopPlacementMath.HasExceededThreshold(start, new Point(31, 20), 10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OcclusionHelpers_DetectPointAndRectOverlap()
|
||||
{
|
||||
var libraryBounds = new Rect(100, 100, 200, 160);
|
||||
|
||||
Assert.True(DesktopPlacementMath.IsOccludedByComponentLibrary(new Point(120, 150), libraryBounds));
|
||||
Assert.False(DesktopPlacementMath.IsOccludedByComponentLibrary(new Point(80, 90), libraryBounds));
|
||||
Assert.True(DesktopPlacementMath.IsOccludedByComponentLibrary(new Rect(250, 120, 120, 80), libraryBounds));
|
||||
Assert.False(DesktopPlacementMath.IsOccludedByComponentLibrary(new Rect(10, 10, 40, 40), libraryBounds));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetSnappedCell_ClampsInsideGridBounds()
|
||||
{
|
||||
var grid = new DesktopGridGeometry(
|
||||
Origin: default,
|
||||
CellSize: 80,
|
||||
CellGap: 8,
|
||||
ColumnCount: 4,
|
||||
RowCount: 5);
|
||||
|
||||
var result = DesktopPlacementMath.TryGetSnappedCell(
|
||||
grid,
|
||||
pointerInViewport: new Point(490, 520),
|
||||
pointerOffset: new Point(10, 10),
|
||||
widthCells: 2,
|
||||
heightCells: 3,
|
||||
out var column,
|
||||
out var row);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(2, column);
|
||||
Assert.Equal(2, row);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCellRect_MapsCellsToPixelRect()
|
||||
{
|
||||
var grid = new DesktopGridGeometry(
|
||||
Origin: new Point(12, 24),
|
||||
CellSize: 80,
|
||||
CellGap: 8,
|
||||
ColumnCount: 6,
|
||||
RowCount: 8);
|
||||
|
||||
var rect = DesktopPlacementMath.GetCellRect(grid, column: 2, row: 3, widthCells: 2, heightCells: 3);
|
||||
|
||||
Assert.Equal(188, rect.X, 3);
|
||||
Assert.Equal(288, rect.Y, 3);
|
||||
Assert.Equal(168, rect.Width, 3);
|
||||
Assert.Equal(256, rect.Height, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Session_DoesNotCommitWhilePointerIsStillInsideLibrary()
|
||||
{
|
||||
var session = DesktopEditSession.CreatePendingNew(
|
||||
componentId: "demo",
|
||||
pageIndex: 0,
|
||||
widthCells: 2,
|
||||
heightCells: 2,
|
||||
startPointerInViewport: new Point(80, 80),
|
||||
pointerOffsetInViewport: new Point(60, 60),
|
||||
componentLibraryBounds: new Rect(0, 0, 220, 300));
|
||||
|
||||
session = session.WithCurrentPointer(new Point(130, 150));
|
||||
|
||||
Assert.True(session.HasExceededThreshold(DesktopPlacementMath.ComputeDragStartThreshold(80)));
|
||||
Assert.True(session.IsPointerInsideComponentLibrary());
|
||||
Assert.False(session.CanCommit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Session_ResizePreviewStillBlocksWhenPointerRemainsInsideLibrary()
|
||||
{
|
||||
var session = DesktopEditSession.CreateResizingExisting(
|
||||
componentId: "demo",
|
||||
placementId: "placement-1",
|
||||
pageIndex: 0,
|
||||
widthCells: 2,
|
||||
heightCells: 2,
|
||||
startPointerInViewport: new Point(80, 80),
|
||||
componentLibraryBounds: new Rect(0, 0, 220, 300))
|
||||
.WithCurrentPointer(new Point(130, 150));
|
||||
|
||||
Assert.True(session.IsPointerInsideComponentLibrary());
|
||||
Assert.False(session.CanCommit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasCellPositionChanged_DetectsNoOpAndRealMoves()
|
||||
{
|
||||
Assert.False(DesktopPlacementMath.HasCellPositionChanged(2, 3, 2, 3));
|
||||
Assert.True(DesktopPlacementMath.HasCellPositionChanged(2, 3, 2, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasCellSpanChanged_DetectsNoOpAndRealResizes()
|
||||
{
|
||||
Assert.False(DesktopPlacementMath.HasCellSpanChanged(2, 3, 2, 3));
|
||||
Assert.True(DesktopPlacementMath.HasCellSpanChanged(2, 3, 3, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCommitPlacement_BlocksWhenPlacementIsOccludedByLibrary()
|
||||
{
|
||||
var placementRect = new Rect(160, 110, 180, 140);
|
||||
var occludingLibraryBounds = new Rect(120, 80, 240, 220);
|
||||
var distantLibraryBounds = new Rect(420, 420, 80, 80);
|
||||
|
||||
Assert.False(DesktopPlacementMath.CanCommitPlacement(placementRect, occludingLibraryBounds));
|
||||
Assert.True(DesktopPlacementMath.CanCommitPlacement(placementRect, distantLibraryBounds));
|
||||
Assert.True(DesktopPlacementMath.CanCommitPlacement(placementRect, componentLibraryBounds: null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Session_AllowsCommitWhenComponentLibraryBoundsAreCleared()
|
||||
{
|
||||
var pendingSession = DesktopEditSession.CreatePendingNew(
|
||||
componentId: "demo",
|
||||
pageIndex: 0,
|
||||
widthCells: 2,
|
||||
heightCells: 2,
|
||||
startPointerInViewport: new Point(80, 80),
|
||||
pointerOffsetInViewport: new Point(60, 60),
|
||||
componentLibraryBounds: null)
|
||||
.WithCurrentPointer(new Point(200, 180));
|
||||
|
||||
Assert.True(pendingSession.HasExceededThreshold(DesktopPlacementMath.ComputeDragStartThreshold(80)));
|
||||
Assert.False(pendingSession.IsPointerInsideComponentLibrary());
|
||||
Assert.False(pendingSession.IsPreviewOccludedByComponentLibrary(new Rect(100, 100, 40, 40)));
|
||||
Assert.False(pendingSession.CanCommit);
|
||||
|
||||
var resizeSession = DesktopEditSession.CreateResizingExisting(
|
||||
componentId: "demo",
|
||||
placementId: "placement-1",
|
||||
pageIndex: 0,
|
||||
widthCells: 2,
|
||||
heightCells: 2,
|
||||
startPointerInViewport: new Point(80, 80),
|
||||
componentLibraryBounds: null)
|
||||
.WithCurrentPointer(new Point(200, 180))
|
||||
.WithTargetCell(row: 2, column: 3);
|
||||
|
||||
Assert.False(resizeSession.IsPointerInsideComponentLibrary());
|
||||
Assert.False(resizeSession.IsPreviewOccludedByComponentLibrary(new Rect(100, 100, 40, 40)));
|
||||
Assert.True(resizeSession.CanCommit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Collections.Generic;
|
||||
using LanMountainDesktop.Appearance;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Host.Abstractions;
|
||||
using LanMountainDesktop.Views.Components;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class InfoRecommendationHostCornerRadiusTests
|
||||
{
|
||||
private static readonly string[] WideInfoComponentIds =
|
||||
[
|
||||
BuiltInComponentIds.DesktopDailyPoetry,
|
||||
BuiltInComponentIds.DesktopDailyArtwork,
|
||||
BuiltInComponentIds.DesktopDailyWord,
|
||||
BuiltInComponentIds.DesktopCnrDailyNews,
|
||||
BuiltInComponentIds.DesktopIfengNews,
|
||||
BuiltInComponentIds.DesktopBilibiliHotSearch,
|
||||
BuiltInComponentIds.DesktopBaiduHotSearch,
|
||||
BuiltInComponentIds.DesktopStcn24Forum
|
||||
];
|
||||
|
||||
[Theory]
|
||||
[InlineData(80d)]
|
||||
[InlineData(120d)]
|
||||
[InlineData(160d)]
|
||||
public void InfoHostRegistrations_ResolveToTheUnifiedLgBaseline(double cellSize)
|
||||
{
|
||||
var registry = new DesktopComponentRuntimeRegistry(
|
||||
ComponentRegistry.CreateDefault(),
|
||||
DesktopComponentRuntimeRegistry.GetDefaultRegistrations());
|
||||
|
||||
foreach (var componentId in WideInfoComponentIds)
|
||||
{
|
||||
AssertResolved(registry, componentId, cellSize);
|
||||
}
|
||||
|
||||
AssertResolved(registry, BuiltInComponentIds.DesktopDailyWord2x2, cellSize);
|
||||
}
|
||||
|
||||
private static void AssertResolved(
|
||||
DesktopComponentRuntimeRegistry registry,
|
||||
string componentId,
|
||||
double cellSize)
|
||||
{
|
||||
Assert.True(
|
||||
registry.TryGetDescriptor(componentId, out var descriptor),
|
||||
$"Missing runtime registration for '{componentId}'.");
|
||||
|
||||
var zero = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 0d));
|
||||
var unit = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 1d));
|
||||
var max = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 2.5d));
|
||||
|
||||
Assert.Equal(0d, zero, 3);
|
||||
Assert.Equal(24d, unit, 3);
|
||||
Assert.Equal(60d, max, 3);
|
||||
Assert.True(zero <= unit && unit <= max);
|
||||
}
|
||||
|
||||
private static ComponentChromeContext CreateChromeContext(
|
||||
string componentId,
|
||||
double cellSize,
|
||||
double globalScale)
|
||||
{
|
||||
return new ComponentChromeContext(
|
||||
componentId,
|
||||
null,
|
||||
cellSize,
|
||||
globalScale,
|
||||
AppearanceCornerRadiusTokenFactory.Create(globalScale));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<Version>1.0.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class WhiteboardNotePersistenceServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void SaveNote_ThenLoadNote_RoundTripsSnapshot()
|
||||
{
|
||||
using var sandbox = new WhiteboardNotePersistenceSandbox();
|
||||
var service = sandbox.CreateService();
|
||||
var snapshot = CreateSampleSnapshot();
|
||||
|
||||
service.SaveNote("DesktopWhiteboard", "whiteboard-1", snapshot, retentionDays: 15);
|
||||
|
||||
var loaded = service.LoadNote("DesktopWhiteboard", "whiteboard-1", retentionDays: 15);
|
||||
|
||||
Assert.Single(loaded.Strokes);
|
||||
Assert.Equal(2, loaded.Strokes[0].Points.Count);
|
||||
Assert.Equal("#FF112233", loaded.Strokes[0].Color);
|
||||
Assert.True(loaded.SavedUtc > DateTimeOffset.MinValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadNote_RemovesExpiredSnapshot_WhenRetentionExceeded()
|
||||
{
|
||||
using var sandbox = new WhiteboardNotePersistenceSandbox();
|
||||
var service = sandbox.CreateService();
|
||||
|
||||
service.SaveNote("DesktopWhiteboard", "expired-board", CreateSampleSnapshot(), retentionDays: 7);
|
||||
sandbox.OverrideSavedTimestamp("DesktopWhiteboard", "expired-board", DateTimeOffset.UtcNow.AddDays(-10), retentionDays: 7);
|
||||
|
||||
var loaded = service.LoadNote("DesktopWhiteboard", "expired-board", retentionDays: 7);
|
||||
|
||||
Assert.Empty(loaded.Strokes);
|
||||
Assert.False(sandbox.Exists("DesktopWhiteboard", "expired-board"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteExpiredNotesBatch_RemovesExpiredRows_AndKeepsFreshRows()
|
||||
{
|
||||
using var sandbox = new WhiteboardNotePersistenceSandbox();
|
||||
var service = sandbox.CreateService();
|
||||
|
||||
service.SaveNote("DesktopWhiteboard", "expired-a", CreateSampleSnapshot(), retentionDays: 7);
|
||||
service.SaveNote("DesktopWhiteboard", "expired-b", CreateSampleSnapshot(), retentionDays: 7);
|
||||
service.SaveNote("DesktopWhiteboard", "fresh-c", CreateSampleSnapshot(), retentionDays: 15);
|
||||
|
||||
sandbox.OverrideSavedTimestamp("DesktopWhiteboard", "expired-a", DateTimeOffset.UtcNow.AddDays(-9), retentionDays: 7);
|
||||
sandbox.OverrideSavedTimestamp("DesktopWhiteboard", "expired-b", DateTimeOffset.UtcNow.AddDays(-8), retentionDays: 7);
|
||||
sandbox.OverrideSavedTimestamp("DesktopWhiteboard", "fresh-c", DateTimeOffset.UtcNow.AddDays(-2), retentionDays: 15);
|
||||
|
||||
var deletedCount = service.DeleteExpiredNotesBatch(batchSize: 10);
|
||||
|
||||
Assert.Equal(2, deletedCount);
|
||||
Assert.False(sandbox.Exists("DesktopWhiteboard", "expired-a"));
|
||||
Assert.False(sandbox.Exists("DesktopWhiteboard", "expired-b"));
|
||||
Assert.True(sandbox.Exists("DesktopWhiteboard", "fresh-c"));
|
||||
}
|
||||
|
||||
private static WhiteboardNoteSnapshot CreateSampleSnapshot()
|
||||
{
|
||||
return new WhiteboardNoteSnapshot
|
||||
{
|
||||
Strokes =
|
||||
[
|
||||
new WhiteboardStrokeSnapshot
|
||||
{
|
||||
Color = "#FF112233",
|
||||
InkThickness = 3.5d,
|
||||
IgnorePressure = true,
|
||||
Points =
|
||||
[
|
||||
new WhiteboardStylusPointSnapshot { X = 12, Y = 34, Pressure = 0.4d, Width = 2, Height = 2 },
|
||||
new WhiteboardStylusPointSnapshot { X = 48, Y = 64, Pressure = 0.7d, Width = 2, Height = 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class WhiteboardNotePersistenceSandbox : IDisposable
|
||||
{
|
||||
private readonly string _directoryPath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"LanMountainDesktop.WhiteboardNoteTests",
|
||||
Guid.NewGuid().ToString("N"));
|
||||
|
||||
private readonly string _databasePath;
|
||||
|
||||
public WhiteboardNotePersistenceSandbox()
|
||||
{
|
||||
Directory.CreateDirectory(_directoryPath);
|
||||
_databasePath = Path.Combine(_directoryPath, "whiteboard-tests.db");
|
||||
}
|
||||
|
||||
public WhiteboardNotePersistenceService CreateService()
|
||||
{
|
||||
return new WhiteboardNotePersistenceService(new AppDatabaseService(_databasePath));
|
||||
}
|
||||
|
||||
public void OverrideSavedTimestamp(string componentId, string placementId, DateTimeOffset savedUtc, int retentionDays)
|
||||
{
|
||||
var expiresUtc = savedUtc.AddDays(WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays));
|
||||
using var connection = new AppDatabaseService(_databasePath).OpenConnection();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = """
|
||||
UPDATE whiteboard_notes
|
||||
SET saved_at_utc_ms = $savedAtUtcMs,
|
||||
expires_at_utc_ms = $expiresAtUtcMs,
|
||||
updated_at_utc_ms = $updatedAtUtcMs
|
||||
WHERE component_id = $componentId
|
||||
AND placement_id = $placementId;
|
||||
""";
|
||||
command.Parameters.AddWithValue("$savedAtUtcMs", savedUtc.ToUnixTimeMilliseconds());
|
||||
command.Parameters.AddWithValue("$expiresAtUtcMs", expiresUtc.ToUnixTimeMilliseconds());
|
||||
command.Parameters.AddWithValue("$updatedAtUtcMs", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||
command.Parameters.AddWithValue("$componentId", componentId);
|
||||
command.Parameters.AddWithValue("$placementId", placementId);
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public bool Exists(string componentId, string placementId)
|
||||
{
|
||||
using var connection = new AppDatabaseService(_databasePath).OpenConnection();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = """
|
||||
SELECT COUNT(1)
|
||||
FROM whiteboard_notes
|
||||
WHERE component_id = $componentId
|
||||
AND placement_id = $placementId;
|
||||
""";
|
||||
command.Parameters.AddWithValue("$componentId", componentId);
|
||||
command.Parameters.AddWithValue("$placementId", placementId);
|
||||
return Convert.ToInt32(command.ExecuteScalar()) > 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_directoryPath))
|
||||
{
|
||||
Directory.Delete(_directoryPath, true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Temporary test directories are best-effort cleanup.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
<Solution>
|
||||
<Project Path="LanAirApp/tools/LanMountainDesktop.PluginPackager/LanMountainDesktop.PluginPackager.csproj" />
|
||||
<Project Path="LanMountainDesktop.Host.Abstractions/LanMountainDesktop.Host.Abstractions.csproj" />
|
||||
<Project Path="LanMountainDesktop.Shared.Contracts/LanMountainDesktop.Shared.Contracts.csproj" />
|
||||
<Project Path="LanMountainDesktop.Settings.Core/LanMountainDesktop.Settings.Core.csproj" />
|
||||
<Project Path="LanMountainDesktop.Appearance/LanMountainDesktop.Appearance.csproj" />
|
||||
<Project Path="LanMountainDesktop.DesktopComponents.Runtime/LanMountainDesktop.DesktopComponents.Runtime.csproj" />
|
||||
<Project Path="LanMountainDesktop.DesktopHost/LanMountainDesktop.DesktopHost.csproj" />
|
||||
<Project Path="LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj" />
|
||||
<Project Path="LanMountainDesktop.PluginTemplate/LanMountainDesktop.PluginTemplate.csproj" />
|
||||
<Project Path="LanMountainDesktop.PluginsInstallHelper/LanMountainDesktop.PluginsInstallHelper.csproj" />
|
||||
<Project Path="LanMountainDesktop/LanMountainDesktop.csproj" />
|
||||
<Project Path="LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj" />
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
<Application.Resources>
|
||||
<FontFamily x:Key="AppFontFamily">avares://LanMountainDesktop/Assets/Fonts#MiSans</FontFamily>
|
||||
<FontFamily x:Key="AppFontFamilyJP">avares://LanMountainDesktop/Assets/Fonts#MiSans JP</FontFamily>
|
||||
<FontFamily x:Key="AppFontFamilyKR">avares://LanMountainDesktop/Assets/Fonts#MiSans KR</FontFamily>
|
||||
</Application.Resources>
|
||||
|
||||
<Application.DataTemplates>
|
||||
@@ -23,6 +25,7 @@
|
||||
<StyleInclude Source="avares://LanMountainDesktop/Styles/GlassModule.axaml" />
|
||||
<StyleInclude Source="avares://LanMountainDesktop/Styles/SettingsAnimations.axaml" />
|
||||
<StyleInclude Source="avares://LanMountainDesktop/Styles/SettingsCardStyles.axaml" />
|
||||
<StyleInclude Source="avares://LanMountainDesktop/Styles/NavigationStyles.axaml" />
|
||||
|
||||
<Style Selector="Window">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource AppFontFamily}" />
|
||||
@@ -71,4 +74,5 @@
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
|
||||
</Application>
|
||||
|
||||
@@ -15,6 +15,7 @@ using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using AvaloniaWebView;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.DesktopHost;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
@@ -46,6 +47,7 @@ public partial class App : Application
|
||||
private readonly IAppearanceThemeService _appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
|
||||
private readonly IAppLogoService _appLogoService = HostAppLogoProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly FontFamilyService _fontFamilyService = new();
|
||||
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
|
||||
private readonly IDetachedComponentLibraryWindowService _detachedComponentLibraryWindowService = new DetachedComponentLibraryWindowService();
|
||||
private readonly ILocationService _locationService = HostLocationServiceProvider.GetOrCreate();
|
||||
@@ -56,17 +58,31 @@ public partial class App : Application
|
||||
private DesktopShellState _desktopShellState = DesktopShellState.ForegroundDesktop;
|
||||
private ShutdownIntent _shutdownIntent;
|
||||
|
||||
private TrayIcons? _trayIcons;
|
||||
private TrayIcon? _trayIcon;
|
||||
private NativeMenuItem? _trayShowDesktopMenuItem;
|
||||
private NativeMenuItem? _traySettingsMenuItem;
|
||||
private NativeMenuItem? _trayComponentLibraryMenuItem;
|
||||
private NativeMenuItem? _trayRestartMenuItem;
|
||||
private NativeMenuItem? _trayExitMenuItem;
|
||||
private PluginRuntimeService? _pluginRuntimeService;
|
||||
private MainWindow? _mainWindow;
|
||||
private bool _mainWindowClosed;
|
||||
private bool _uiUnhandledExceptionHooked;
|
||||
private DesktopShellHost? _desktopShellHost;
|
||||
|
||||
internal static SingleInstanceService? CurrentSingleInstanceService { get; set; }
|
||||
internal static (UserBehaviorAnalyticsService?, CrashReportService?) AnalyticsServices { get; set; }
|
||||
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
|
||||
(Current as App)?._hostApplicationLifecycle;
|
||||
|
||||
// 隐私政策查看事件
|
||||
public static event Action? CurrentPrivacyPolicyViewRequested;
|
||||
|
||||
// 触发隐私政策查看事件的方法
|
||||
public static void RaisePrivacyPolicyViewRequested()
|
||||
{
|
||||
CurrentPrivacyPolicyViewRequested?.Invoke();
|
||||
}
|
||||
|
||||
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
|
||||
public ISettingsFacadeService SettingsFacade => _settingsFacade;
|
||||
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
|
||||
@@ -86,6 +102,11 @@ public partial class App : Application
|
||||
|
||||
public App()
|
||||
{
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settingsFacade.Settings.Changed += OnSettingsChanged;
|
||||
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
|
||||
}
|
||||
@@ -93,9 +114,16 @@ public partial class App : Application
|
||||
public override void Initialize()
|
||||
{
|
||||
AppLogger.Info("App", "Initializing application resources.");
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
ApplyDesignTimeTheme();
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigureWebViewUserDataFolder();
|
||||
AvaloniaWebViewBuilder.Initialize(default);
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
ApplyThemeFromSettings();
|
||||
ApplyCurrentCultureFromSettings();
|
||||
EnsureSettingsWindowService();
|
||||
@@ -104,31 +132,55 @@ public partial class App : Application
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
return;
|
||||
}
|
||||
|
||||
AppLogger.Info("App", "Framework initialization completed.");
|
||||
RegisterUiUnhandledExceptionGuard();
|
||||
LinuxDesktopEntryInstaller.EnsureInstalled();
|
||||
InitializePluginRuntime();
|
||||
InitializeTrayIcon();
|
||||
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
private void ApplyDesignTimeTheme()
|
||||
{
|
||||
RequestedThemeVariant = ThemeVariant.Light;
|
||||
|
||||
try
|
||||
{
|
||||
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
|
||||
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
|
||||
DisableAvaloniaDataAnnotationValidation();
|
||||
desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnExplicitShutdown;
|
||||
desktop.Exit += (_, _) =>
|
||||
ApplyAdaptiveThemeResources();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("Previewer", "Failed to apply adaptive theme resources in design mode.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeDesktopShell()
|
||||
{
|
||||
_desktopShellHost ??= new DesktopShellHost(
|
||||
InitializePluginRuntime,
|
||||
InitializeTrayIcon,
|
||||
desktop =>
|
||||
{
|
||||
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
|
||||
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
|
||||
DisableAvaloniaDataAnnotationValidation();
|
||||
desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnExplicitShutdown;
|
||||
CreateAndAssignMainWindow(desktop, "FrameworkInitialization");
|
||||
},
|
||||
() =>
|
||||
{
|
||||
AppLogger.Info("App", "Desktop lifetime exit triggered.");
|
||||
PerformExitCleanup();
|
||||
};
|
||||
|
||||
CreateAndAssignMainWindow(desktop, "FrameworkInitialization");
|
||||
CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow);
|
||||
}
|
||||
|
||||
StartWeatherLocationRefreshIfNeeded();
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
},
|
||||
() => CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow),
|
||||
StartWeatherLocationRefreshIfNeeded);
|
||||
_desktopShellHost.Initialize(this);
|
||||
}
|
||||
|
||||
private void OnTrayExitClick(object? sender, EventArgs e)
|
||||
@@ -229,18 +281,43 @@ public partial class App : Application
|
||||
{
|
||||
try
|
||||
{
|
||||
DisposeTrayIcon();
|
||||
|
||||
var trayIcon = new TrayIcon
|
||||
if (_trayIcon is null)
|
||||
{
|
||||
Icon = _appLogoService.CreateTrayIcon(),
|
||||
ToolTipText = L("tray.tooltip", "LanMountainDesktop"),
|
||||
Menu = BuildTrayMenu(),
|
||||
IsVisible = true
|
||||
};
|
||||
_trayShowDesktopMenuItem = new NativeMenuItem();
|
||||
_trayShowDesktopMenuItem.Click += OnTrayShowDesktopClick;
|
||||
|
||||
_trayIcons = [trayIcon];
|
||||
TrayIcon.SetIcons(this, _trayIcons);
|
||||
_traySettingsMenuItem = new NativeMenuItem();
|
||||
_traySettingsMenuItem.Click += OnTraySettingsClick;
|
||||
|
||||
_trayComponentLibraryMenuItem = new NativeMenuItem();
|
||||
_trayComponentLibraryMenuItem.Click += OnTrayComponentLibraryClick;
|
||||
|
||||
_trayRestartMenuItem = new NativeMenuItem();
|
||||
_trayRestartMenuItem.Click += OnTrayRestartClick;
|
||||
|
||||
_trayExitMenuItem = new NativeMenuItem();
|
||||
_trayExitMenuItem.Click += OnTrayExitClick;
|
||||
|
||||
var trayMenu = new NativeMenu();
|
||||
trayMenu.Items.Add(_trayShowDesktopMenuItem);
|
||||
trayMenu.Items.Add(_traySettingsMenuItem);
|
||||
trayMenu.Items.Add(_trayComponentLibraryMenuItem);
|
||||
trayMenu.Items.Add(new NativeMenuItemSeparator());
|
||||
trayMenu.Items.Add(_trayRestartMenuItem);
|
||||
trayMenu.Items.Add(new NativeMenuItemSeparator());
|
||||
trayMenu.Items.Add(_trayExitMenuItem);
|
||||
|
||||
_trayIcon = new TrayIcon
|
||||
{
|
||||
Icon = _appLogoService.CreateTrayIcon(),
|
||||
Menu = trayMenu,
|
||||
IsVisible = true
|
||||
};
|
||||
|
||||
TrayIcon.SetIcons(this, [_trayIcon]);
|
||||
}
|
||||
|
||||
RefreshTrayIconContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -248,51 +325,58 @@ public partial class App : Application
|
||||
}
|
||||
}
|
||||
|
||||
private NativeMenu BuildTrayMenu()
|
||||
private void RefreshTrayIconContent()
|
||||
{
|
||||
var menu = new NativeMenu();
|
||||
if (_trayIcon is not null)
|
||||
{
|
||||
_trayIcon.IsVisible = true;
|
||||
if (!OperatingSystem.IsLinux())
|
||||
{
|
||||
_trayIcon.ToolTipText = L("tray.tooltip", "LanMountainDesktop");
|
||||
}
|
||||
}
|
||||
|
||||
var showDesktopItem = new NativeMenuItem(L("tray.menu.show_desktop", "Open Desktop"));
|
||||
showDesktopItem.Click += OnTrayShowDesktopClick;
|
||||
menu.Items.Add(showDesktopItem);
|
||||
if (_trayShowDesktopMenuItem is not null)
|
||||
{
|
||||
_trayShowDesktopMenuItem.Header = L("tray.menu.show_desktop", "Open Desktop");
|
||||
}
|
||||
|
||||
var settingsItem = new NativeMenuItem(L("tray.menu.settings", "Settings"));
|
||||
settingsItem.Click += OnTraySettingsClick;
|
||||
menu.Items.Add(settingsItem);
|
||||
if (_traySettingsMenuItem is not null)
|
||||
{
|
||||
_traySettingsMenuItem.Header = L("tray.menu.settings", "Settings");
|
||||
}
|
||||
|
||||
var componentLibraryItem = new NativeMenuItem(L("tray.menu.component_library", "Component Library"));
|
||||
componentLibraryItem.Click += OnTrayComponentLibraryClick;
|
||||
menu.Items.Add(componentLibraryItem);
|
||||
if (_trayComponentLibraryMenuItem is not null)
|
||||
{
|
||||
_trayComponentLibraryMenuItem.Header = L("tray.menu.component_library", "Component Library");
|
||||
}
|
||||
|
||||
menu.Items.Add(new NativeMenuItemSeparator());
|
||||
if (_trayRestartMenuItem is not null)
|
||||
{
|
||||
_trayRestartMenuItem.Header = L("tray.menu.restart", "Restart App");
|
||||
}
|
||||
|
||||
var restartItem = new NativeMenuItem(L("tray.menu.restart", "Restart App"));
|
||||
restartItem.Click += OnTrayRestartClick;
|
||||
menu.Items.Add(restartItem);
|
||||
|
||||
menu.Items.Add(new NativeMenuItemSeparator());
|
||||
|
||||
var exitItem = new NativeMenuItem(L("tray.menu.exit", "Exit App"));
|
||||
exitItem.Click += OnTrayExitClick;
|
||||
menu.Items.Add(exitItem);
|
||||
|
||||
return menu;
|
||||
if (_trayExitMenuItem is not null)
|
||||
{
|
||||
_trayExitMenuItem.Header = L("tray.menu.exit", "Exit App");
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeTrayIcon()
|
||||
{
|
||||
if (_trayIcons is null)
|
||||
if (_trayIcon is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TrayIcon.SetIcons(this, null);
|
||||
foreach (var trayIcon in _trayIcons)
|
||||
try
|
||||
{
|
||||
trayIcon.Dispose();
|
||||
_trayIcon.IsVisible = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("TrayIcon", "Failed to hide tray icon during cleanup.", ex);
|
||||
}
|
||||
|
||||
_trayIcons = null;
|
||||
}
|
||||
|
||||
private void EnsureSettingsWindowService()
|
||||
@@ -365,6 +449,21 @@ public partial class App : Application
|
||||
CultureInfo.DefaultThreadCurrentUICulture = culture;
|
||||
Thread.CurrentThread.CurrentCulture = culture;
|
||||
Thread.CurrentThread.CurrentUICulture = culture;
|
||||
|
||||
ApplyLanguageSpecificFont(languageCode);
|
||||
}
|
||||
|
||||
private void ApplyLanguageSpecificFont(string languageCode)
|
||||
{
|
||||
var fontFamily = _fontFamilyService.GetFontFamilyForLanguage(languageCode);
|
||||
if (Resources.TryGetValue("AppFontFamily", out var currentFont) &&
|
||||
currentFont is FontFamily currentFontFamily &&
|
||||
currentFontFamily.Name == fontFamily.Name)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Resources["AppFontFamily"] = fontFamily;
|
||||
}
|
||||
|
||||
private void ActivateMainWindow()
|
||||
@@ -484,6 +583,7 @@ public partial class App : Application
|
||||
refreshAll ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale), StringComparer.OrdinalIgnoreCase) ||
|
||||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) ||
|
||||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
|
||||
@@ -501,11 +601,10 @@ public partial class App : Application
|
||||
|
||||
if (languageChanged)
|
||||
{
|
||||
// 清除本地化缓存,强制重新加载语言文件
|
||||
_localizationService.ClearCache();
|
||||
ApplyCurrentCultureFromSettings();
|
||||
if (_trayIcons is not null)
|
||||
{
|
||||
InitializeTrayIcon();
|
||||
}
|
||||
RefreshTrayIconContent();
|
||||
}
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
@@ -573,13 +672,13 @@ public partial class App : Application
|
||||
|
||||
try
|
||||
{
|
||||
var (analytics, crashReport) = App.AnalyticsServices;
|
||||
analytics?.SendShutdownEvent();
|
||||
crashReport?.SendShutdownEvent();
|
||||
TelemetryServices.Usage?.Shutdown(
|
||||
_shutdownIntent == ShutdownIntent.RestartRequested,
|
||||
"App.PerformExitCleanup");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("Analytics", "Failed to send shutdown events during exit cleanup.", ex);
|
||||
AppLogger.Warn("Analytics", "Failed to shut down usage telemetry during exit cleanup.", ex);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -613,6 +712,27 @@ public partial class App : Application
|
||||
AudioRecorderServiceFactory.DisposeSharedServices();
|
||||
StudyAnalyticsServiceFactory.DisposeSharedService();
|
||||
DisposeTrayIcon();
|
||||
|
||||
try
|
||||
{
|
||||
TelemetryServices.Crash?.CaptureShutdown(
|
||||
_shutdownIntent == ShutdownIntent.RestartRequested,
|
||||
"App.PerformExitCleanup");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("Analytics", "Failed to capture crash shutdown telemetry during exit cleanup.", ex);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
TelemetryServices.Crash?.Dispose();
|
||||
TelemetryServices.Usage?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("Analytics", "Failed to dispose telemetry services during exit cleanup.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private MainWindow CreateAndAssignMainWindow(
|
||||
|
||||
229
LanMountainDesktop/Assets/Documents/Privacy.md
Normal file
229
LanMountainDesktop/Assets/Documents/Privacy.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# LanMountainDesktop 遥测隐私政策
|
||||
|
||||
**生效日期**:2026年3月22日\
|
||||
**最后更新**:2026年3月22日
|
||||
|
||||
***
|
||||
|
||||
## 引言
|
||||
|
||||
LanMountainDesktop(以下简称"本应用")由 灵方软件Lincube(以下简称"我们")开发和维护。我们深知用户隐私的重要性,并致力于保护您的个人信息安全。本隐私政策旨在向您说明我们如何收集、使用、存储和保护您的数据。
|
||||
|
||||
使用本应用即表示您同意本隐私政策的条款。如果您不同意本政策的任何部分,请停止使用本应用。
|
||||
|
||||
***
|
||||
|
||||
## 1. 数据收集范围
|
||||
|
||||
### 1.1 我们收集的数据
|
||||
|
||||
本应用提供两类可选的数据收集功能:
|
||||
|
||||
| 数据类型 | 收集方式 | 默认状态 | 用途 |
|
||||
| ------ | ---- | ---- | -------- |
|
||||
| 启动基线事件 | 自动收集 | 开启 | 统计用户量 |
|
||||
| 崩溃数据 | 用户授权 | 关闭 | 分析稳定性问题 |
|
||||
| 行为数据 | 用户授权 | 关闭 | 分析功能使用情况 |
|
||||
|
||||
### 1.2 启动基线事件
|
||||
|
||||
无论您是否开启其他遥测选项,本应用会在首次启动时发送一次最小化的启动基线事件(`app_first_launch`),用于统计活跃用户量。该事件仅包含:
|
||||
|
||||
- 匿名安装标识符(Install ID)
|
||||
- 应用版本号
|
||||
- 启动时间戳
|
||||
|
||||
### 1.3 崩溃数据
|
||||
|
||||
当您开启"崩溃数据上传"功能时,我们可能收集以下信息:
|
||||
|
||||
- **异常信息**:异常类型、错误消息、堆栈跟踪
|
||||
- **应用信息**:应用版本、构建号、运行时环境
|
||||
- **系统信息**:操作系统版本、系统架构、可用内存
|
||||
- **设备信息**:设备型号、屏幕分辨率
|
||||
- **日志信息**:应用崩溃前的最近日志记录(可能包含您在使用过程中产生的操作记录)
|
||||
|
||||
### 1.4 行为数据
|
||||
|
||||
当您开启"行为数据分析"功能时,我们可能收集以下信息:
|
||||
|
||||
- **会话信息**:应用启动/退出时间、会话持续时间
|
||||
- **功能使用**:设置页面访问、抽屉操作、组件库操作
|
||||
- **组件操作**:桌面组件的放置、移动、调整大小、删除操作
|
||||
- **界面交互**:页面切换、编辑模式进入/退出
|
||||
|
||||
***
|
||||
|
||||
## 2. 数据使用目的
|
||||
|
||||
我们收集的数据将用于以下目的:
|
||||
|
||||
### 2.1 启动基线事件
|
||||
|
||||
- 统计应用的用户数量和活跃度
|
||||
- 了解应用的安装分布情况
|
||||
|
||||
### 2.2 崩溃数据
|
||||
|
||||
- 诊断和修复应用崩溃问题
|
||||
- 提高应用的稳定性和可靠性
|
||||
- 识别和解决性能瓶颈
|
||||
|
||||
### 2.3 行为数据
|
||||
|
||||
- 了解用户如何使用本应用的功能
|
||||
- 改进用户体验和界面设计
|
||||
- 指导功能开发和优先级决策
|
||||
- 分析用户行为模式和趋势
|
||||
|
||||
***
|
||||
|
||||
## 3. 数据存储与传输
|
||||
|
||||
### 3.1 数据传输
|
||||
|
||||
您的数据将通过加密连接传输至以下第三方服务:
|
||||
|
||||
| 服务提供商 | 服务类型 | 数据内容 | 隐私政策 |
|
||||
| ------- | ---- | --------- | ----------------------------- |
|
||||
| PostHog | 产品分析 | 启动事件、行为数据 | <https://posthog.com/privacy> |
|
||||
| Sentry | 错误监控 | 崩溃数据、异常信息 | <https://sentry.io/privacy/> |
|
||||
|
||||
### 3.2 数据存储位置
|
||||
|
||||
数据存储于上述第三方服务的服务器,这些服务器可能位于中国境外。我们已与这些服务提供商签订数据处理协议,确保您的数据得到适当保护。
|
||||
|
||||
### 3.3 数据保留期限
|
||||
|
||||
- **启动基线事件**:保留期限由 PostHog 服务配置决定,通常为 13 个月
|
||||
- **崩溃数据**:保留期限由 Sentry 服务配置决定,通常为 90 天
|
||||
- **行为数据**:保留期限由 PostHog 服务配置决定,通常为 13 个月
|
||||
|
||||
***
|
||||
|
||||
## 4. 用户权利与控制
|
||||
|
||||
### 4.1 您的权利
|
||||
|
||||
根据适用的数据保护法律,您享有以下权利:
|
||||
|
||||
- **知情权**:了解我们收集哪些数据及其用途
|
||||
- **访问权**:请求获取我们持有的您的个人数据副本
|
||||
- **更正权**:请求更正不准确或不完整的个人数据
|
||||
- **删除权**:请求删除您的个人数据
|
||||
- **撤回同意权**:随时撤回您对数据收集的同意
|
||||
- **数据可携带权**:以结构化格式接收您的个人数据
|
||||
|
||||
### 4.2 如何行使您的权利
|
||||
|
||||
您可以通过以下方式行使上述权利:
|
||||
|
||||
1. **关闭遥测功能**:在应用设置 > 隐私设置中关闭相应开关
|
||||
2. **刷新遥测标识**:在应用设置 > 隐私设置中点击"刷新遥测 ID"
|
||||
3. **联系我们**:通过 GitHub Issues 提交数据相关请求
|
||||
|
||||
### 4.3 功能控制
|
||||
|
||||
| 功能 | 控制方式 | 效果 |
|
||||
| ------- | ---- | ----------- |
|
||||
| 崩溃数据上传 | 设置开关 | 关闭后停止发送崩溃数据 |
|
||||
| 行为数据分析 | 设置开关 | 关闭后停止发送行为数据 |
|
||||
| 刷新遥测 ID | 手动触发 | 生成新的匿名标识符 |
|
||||
|
||||
***
|
||||
|
||||
## 5. 身份标识
|
||||
|
||||
### 5.1 匿名标识符
|
||||
|
||||
我们使用以下匿名标识符来区分用户和会话:
|
||||
|
||||
- **Install ID**:在应用首次安装时随机生成的唯一标识符,用于区分不同的安装实例
|
||||
- **Telemetry ID**:匿名标识符,用于关联遥测数据
|
||||
|
||||
### 5.2 标识符特性
|
||||
|
||||
- 这些标识符不包含您的真实身份信息
|
||||
- 标识符与您的个人身份(如姓名、邮箱、电话)无关联
|
||||
|
||||
***
|
||||
|
||||
## 6. 数据安全
|
||||
|
||||
### 6.1 安全措施
|
||||
|
||||
我们采取以下安全措施保护您的数据:
|
||||
|
||||
- **传输加密**:所有数据传输均使用 TLS/HTTPS 加密
|
||||
- **访问控制**:限制对数据的访问权限,仅授权人员可访问
|
||||
- **匿名化处理**:使用匿名标识符而非个人身份信息
|
||||
|
||||
### 6.2 数据泄露响应
|
||||
|
||||
如发生数据泄露事件,我们将:
|
||||
|
||||
1. 及时评估泄露的影响范围和严重程度
|
||||
2. 采取必要措施阻止进一步泄露
|
||||
3. 根据法律要求通知相关监管机构和受影响用户
|
||||
|
||||
***
|
||||
|
||||
## 7. 第三方服务
|
||||
|
||||
### 7.1 PostHog
|
||||
|
||||
PostHog 是我们使用的产品分析平台,用于收集和分析用户行为数据。PostHog 的隐私政策请参阅:<https://posthog.com/privacy>
|
||||
|
||||
### 7.2 Sentry
|
||||
|
||||
Sentry 是我们使用的错误监控平台,用于收集和分析崩溃数据。Sentry 的隐私政策请参阅:<https://sentry.io/privacy/>
|
||||
|
||||
### 7.3 第三方责任
|
||||
|
||||
我们仅将上述第三方服务用于本政策所述目的。我们不对这些第三方的隐私实践负责,建议您阅读其隐私政策。
|
||||
|
||||
***
|
||||
|
||||
## 8. 儿童隐私
|
||||
|
||||
本应用不面向 14 周岁以下的儿童。我们不会故意收集儿童的个人信息。如果您是 14 周岁以下儿童的监护人,且发现您的孩子向我们提供了个人信息,请联系我们,我们将采取措施删除相关信息。
|
||||
|
||||
***
|
||||
|
||||
## 9. 隐私政策更新
|
||||
|
||||
我们可能会不时更新本隐私政策。更新后的政策将在本应用内发布,并在政策顶部注明"最后更新"日期。重大变更时,我们将在应用内通过显著方式通知您。
|
||||
|
||||
建议您定期查阅本政策,以了解我们如何保护您的信息。继续使用本应用即表示您接受更新后的隐私政策。
|
||||
|
||||
***
|
||||
|
||||
## 10. 适用法律
|
||||
|
||||
本隐私政策的解释和执行适用中华人民共和国法律法规,包括但不限于:
|
||||
|
||||
- 《中华人民共和国个人信息保护法》
|
||||
- 《中华人民共和国数据安全法》
|
||||
- 《中华人民共和国网络安全法》
|
||||
- 《信息安全技术 个人信息安全规范》(GB/T 35273)
|
||||
|
||||
***
|
||||
|
||||
## 11. 联系我们
|
||||
|
||||
如果您对本隐私政策有任何疑问、意见或建议,请通过以下方式联系我们:
|
||||
|
||||
- **GitHub 仓库**:<https://github.com/wwiinnddyy/LanMountainDesktop>
|
||||
- **问题反馈**:<https://github.com/wwiinnddyy/LanMountainDesktop/issues>
|
||||
|
||||
我们将在收到您的请求后 30 日内予以答复。
|
||||
|
||||
***
|
||||
|
||||
## 12. 条款可分割性
|
||||
|
||||
如果本隐私政策的任何条款被有管辖权的法院或监管机构认定为无效或不可执行,该条款应在最小必要范围内进行修改以使其有效和可执行,或如果无法修改,则予以删除。本政策的其余条款将继续有效。
|
||||
|
||||
***
|
||||
|
||||
**本隐私政策最终解释权归灵方软件Lincube所有。**
|
||||
1
LanMountainDesktop/Assets/bilibili.svg
Normal file
1
LanMountainDesktop/Assets/bilibili.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Bilibili</title><path d="M17.813 4.653h.854c1.51.054 2.769.578 3.773 1.574 1.004.995 1.524 2.249 1.56 3.76v7.36c-.036 1.51-.556 2.769-1.56 3.773s-2.262 1.524-3.773 1.56H5.333c-1.51-.036-2.769-.556-3.773-1.56S.036 18.858 0 17.347v-7.36c.036-1.511.556-2.765 1.56-3.76 1.004-.996 2.262-1.52 3.773-1.574h.774l-1.174-1.12a1.234 1.234 0 0 1-.373-.906c0-.356.124-.658.373-.907l.027-.027c.267-.249.573-.373.92-.373.347 0 .653.124.92.373L9.653 4.44c.071.071.134.142.187.213h4.267a.836.836 0 0 1 .16-.213l2.853-2.747c.267-.249.573-.373.92-.373.347 0 .662.151.929.4.267.249.391.551.391.907 0 .355-.124.657-.373.906zM5.333 7.24c-.746.018-1.373.276-1.88.773-.506.498-.769 1.13-.786 1.894v7.52c.017.764.28 1.395.786 1.893.507.498 1.134.756 1.88.773h13.334c.746-.017 1.373-.275 1.88-.773.506-.498.769-1.129.786-1.893v-7.52c-.017-.765-.28-1.396-.786-1.894-.507-.497-1.134-.755-1.88-.773zM8 11.107c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c0-.373.129-.689.386-.947.258-.257.574-.386.947-.386zm8 0c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c.017-.391.15-.711.4-.96.249-.249.56-.373.933-.373Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
LanMountainDesktop/Assets/juya_avatar.jpg
Normal file
BIN
LanMountainDesktop/Assets/juya_avatar.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
1
LanMountainDesktop/Assets/wechat.svg
Normal file
1
LanMountainDesktop/Assets/wechat.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>WeChat</title><path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -33,6 +33,7 @@ public static class BuiltInComponentIds
|
||||
public const string DesktopDailyWord2x2 = "DesktopDailyWord2x2";
|
||||
public const string DesktopCnrDailyNews = "DesktopCnrDailyNews";
|
||||
public const string DesktopIfengNews = "DesktopIfengNews";
|
||||
public const string DesktopJuyaNews = "DesktopJuyaNews";
|
||||
public const string DesktopBilibiliHotSearch = "DesktopBilibiliHotSearch";
|
||||
public const string DesktopBaiduHotSearch = "DesktopBaiduHotSearch";
|
||||
public const string DesktopStcn24Forum = "DesktopStcn24Forum";
|
||||
@@ -41,4 +42,5 @@ public static class BuiltInComponentIds
|
||||
public const string DesktopBlackboardLandscape = "DesktopBlackboardLandscape";
|
||||
public const string DesktopBrowser = "DesktopBrowser";
|
||||
public const string DesktopOfficeRecentDocuments = "DesktopOfficeRecentDocuments";
|
||||
public const string DesktopRemovableStorage = "DesktopRemovableStorage";
|
||||
}
|
||||
|
||||
@@ -261,6 +261,16 @@ public sealed class ComponentRegistry
|
||||
MinHeightCells: 4,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopJuyaNews,
|
||||
"橘鸦早报",
|
||||
"News",
|
||||
"Info",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 4,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true,
|
||||
ResizeMode: DesktopComponentResizeMode.Free),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopBilibiliHotSearch,
|
||||
"Bilibili Hot Search",
|
||||
@@ -336,6 +346,15 @@ public sealed class ComponentRegistry
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopRemovableStorage,
|
||||
"Removable Storage",
|
||||
"Storage",
|
||||
"File",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.Date,
|
||||
"Calendar",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using LanMountainDesktop.Host.Abstractions;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
@@ -10,5 +11,6 @@ public sealed record DesktopComponentRuntimeContext(
|
||||
ISettingsFacadeService SettingsFacade,
|
||||
ISettingsService SettingsService,
|
||||
IAppearanceThemeService AppearanceTheme,
|
||||
ComponentChromeContext Chrome,
|
||||
IComponentSettingsAccessor ComponentSettingsAccessor,
|
||||
IComponentInstanceSettingsStore ComponentSettingsStore);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using LanMountainDesktop.Host.Abstractions;
|
||||
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
public interface IComponentChromeContextAware
|
||||
{
|
||||
void SetComponentChromeContext(ComponentChromeContext context);
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
using Avalonia.Animation.Easings;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace LanMountainDesktop.DesktopEditing;
|
||||
|
||||
internal sealed class ComponentLibraryCollapsePresenter
|
||||
{
|
||||
private static readonly TimeSpan TransitionDuration = TimeSpan.FromMilliseconds(150);
|
||||
private static readonly Easing TransitionEasing = new CubicEaseOut();
|
||||
private const double StableOpacityThreshold = 0.01;
|
||||
|
||||
private readonly Border _componentLibraryWindow;
|
||||
private readonly Border _collapsedChipHost;
|
||||
private readonly TextBlock _collapsedChipTextBlock;
|
||||
private readonly Control? _collapsedChipIcon;
|
||||
private readonly TranslateTransform _windowTranslate = new();
|
||||
private readonly TranslateTransform _chipTranslate = new();
|
||||
private readonly ScaleTransform _chipScale = new(1, 1);
|
||||
|
||||
private ComponentLibraryCollapseState _state;
|
||||
private int _transitionVersion;
|
||||
|
||||
public ComponentLibraryCollapsePresenter(
|
||||
Border componentLibraryWindow,
|
||||
Border collapsedChipHost,
|
||||
TextBlock collapsedChipTextBlock,
|
||||
Control? collapsedChipIcon = null)
|
||||
{
|
||||
_componentLibraryWindow = componentLibraryWindow ?? throw new ArgumentNullException(nameof(componentLibraryWindow));
|
||||
_collapsedChipHost = collapsedChipHost ?? throw new ArgumentNullException(nameof(collapsedChipHost));
|
||||
_collapsedChipTextBlock = collapsedChipTextBlock ?? throw new ArgumentNullException(nameof(collapsedChipTextBlock));
|
||||
_collapsedChipIcon = collapsedChipIcon;
|
||||
|
||||
EnsureTransforms();
|
||||
_state = ComponentLibraryCollapseState.CreateExpanded(
|
||||
_componentLibraryWindow.Margin,
|
||||
_componentLibraryWindow.Opacity <= 0 ? 1 : _componentLibraryWindow.Opacity);
|
||||
ApplyExpandedSnapshot();
|
||||
_collapsedChipHost.IsVisible = false;
|
||||
_collapsedChipHost.IsHitTestVisible = false;
|
||||
_collapsedChipHost.Opacity = 0;
|
||||
}
|
||||
|
||||
public bool IsCollapsed => _state.VisualState is ComponentLibraryCollapseVisualState.Collapsing or ComponentLibraryCollapseVisualState.Collapsed;
|
||||
|
||||
public ComponentLibraryCollapseVisualState VisualState => _state.VisualState;
|
||||
|
||||
public void SyncExpandedState(Thickness margin, double opacity)
|
||||
{
|
||||
var hasStableOpacity = IsStableExpandedOpacity(opacity);
|
||||
var nextExpandedOpacity = hasStableOpacity ? Math.Clamp(opacity, 0, 1) : _state.ExpandedOpacity;
|
||||
_state = _state with
|
||||
{
|
||||
ExpandedMargin = margin,
|
||||
ExpandedOpacity = nextExpandedOpacity
|
||||
};
|
||||
|
||||
if (_state.VisualState is ComponentLibraryCollapseVisualState.Expanded or ComponentLibraryCollapseVisualState.Restoring)
|
||||
{
|
||||
ApplyExpandedSnapshot(applyOpacity: hasStableOpacity);
|
||||
}
|
||||
}
|
||||
|
||||
public void Collapse(string title)
|
||||
{
|
||||
_collapsedChipTextBlock.Text = string.IsNullOrWhiteSpace(title) ? "Widgets" : title;
|
||||
|
||||
if (_state.VisualState is ComponentLibraryCollapseVisualState.Collapsing or ComponentLibraryCollapseVisualState.Collapsed)
|
||||
{
|
||||
ShowCollapsedChip(_transitionVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
var version = ++_transitionVersion;
|
||||
_state = _state.WithVisualState(ComponentLibraryCollapseVisualState.Collapsing, isChipVisible: true);
|
||||
|
||||
ApplyExpandedSnapshot();
|
||||
ShowCollapsedChip(version);
|
||||
SetCollapsedWindowTargets();
|
||||
|
||||
DispatcherTimer.RunOnce(
|
||||
() =>
|
||||
{
|
||||
if (version != _transitionVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_state = _state.WithVisualState(ComponentLibraryCollapseVisualState.Collapsed, isChipVisible: true);
|
||||
_componentLibraryWindow.IsVisible = false;
|
||||
_componentLibraryWindow.IsHitTestVisible = false;
|
||||
},
|
||||
TransitionDuration);
|
||||
}
|
||||
|
||||
public void Restore()
|
||||
{
|
||||
if (_state.VisualState is ComponentLibraryCollapseVisualState.Expanded)
|
||||
{
|
||||
ApplyExpandedSnapshot();
|
||||
_collapsedChipHost.IsVisible = false;
|
||||
_collapsedChipHost.IsHitTestVisible = false;
|
||||
_collapsedChipHost.Opacity = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var version = ++_transitionVersion;
|
||||
_state = _state.WithVisualState(ComponentLibraryCollapseVisualState.Restoring, isChipVisible: false);
|
||||
|
||||
PrepareRestoringWindow();
|
||||
HideCollapsedChip(version);
|
||||
Dispatcher.UIThread.Post(
|
||||
() =>
|
||||
{
|
||||
if (version != _transitionVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_componentLibraryWindow.Opacity = _state.ExpandedOpacity;
|
||||
_windowTranslate.Y = 0;
|
||||
},
|
||||
DispatcherPriority.Background);
|
||||
|
||||
DispatcherTimer.RunOnce(
|
||||
() =>
|
||||
{
|
||||
if (version != _transitionVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_state = _state.WithVisualState(ComponentLibraryCollapseVisualState.Expanded, isChipVisible: false);
|
||||
_componentLibraryWindow.IsVisible = true;
|
||||
_componentLibraryWindow.IsHitTestVisible = true;
|
||||
},
|
||||
TransitionDuration);
|
||||
}
|
||||
|
||||
private void EnsureTransforms()
|
||||
{
|
||||
_componentLibraryWindow.RenderTransform = _windowTranslate;
|
||||
_windowTranslate.Transitions = new Transitions
|
||||
{
|
||||
new DoubleTransition
|
||||
{
|
||||
Property = TranslateTransform.YProperty,
|
||||
Duration = TransitionDuration,
|
||||
Easing = TransitionEasing
|
||||
}
|
||||
};
|
||||
|
||||
_collapsedChipHost.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
|
||||
_collapsedChipHost.RenderTransform = new TransformGroup
|
||||
{
|
||||
Children =
|
||||
{
|
||||
_chipTranslate,
|
||||
_chipScale
|
||||
}
|
||||
};
|
||||
_chipTranslate.Transitions = new Transitions
|
||||
{
|
||||
new DoubleTransition
|
||||
{
|
||||
Property = TranslateTransform.YProperty,
|
||||
Duration = TransitionDuration,
|
||||
Easing = TransitionEasing
|
||||
}
|
||||
};
|
||||
_chipScale.Transitions = new Transitions
|
||||
{
|
||||
new DoubleTransition
|
||||
{
|
||||
Property = ScaleTransform.ScaleXProperty,
|
||||
Duration = TransitionDuration,
|
||||
Easing = TransitionEasing
|
||||
},
|
||||
new DoubleTransition
|
||||
{
|
||||
Property = ScaleTransform.ScaleYProperty,
|
||||
Duration = TransitionDuration,
|
||||
Easing = TransitionEasing
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void ApplyExpandedSnapshot(bool applyOpacity = true)
|
||||
{
|
||||
_componentLibraryWindow.Margin = _state.ExpandedMargin;
|
||||
if (applyOpacity)
|
||||
{
|
||||
_componentLibraryWindow.Opacity = _state.ExpandedOpacity;
|
||||
}
|
||||
|
||||
_componentLibraryWindow.IsVisible = true;
|
||||
_componentLibraryWindow.IsHitTestVisible = true;
|
||||
_windowTranslate.Y = 0;
|
||||
}
|
||||
|
||||
private void SetCollapsedWindowTargets()
|
||||
{
|
||||
_componentLibraryWindow.Opacity = 0;
|
||||
_windowTranslate.Y = 28;
|
||||
}
|
||||
|
||||
private void ShowCollapsedChip(int version)
|
||||
{
|
||||
_collapsedChipHost.IsVisible = true;
|
||||
_collapsedChipHost.IsHitTestVisible = false;
|
||||
_collapsedChipTextBlock.IsVisible = true;
|
||||
if (_collapsedChipIcon is not null)
|
||||
{
|
||||
_collapsedChipIcon.IsVisible = true;
|
||||
}
|
||||
|
||||
_collapsedChipHost.Opacity = 0;
|
||||
_chipTranslate.Y = 8;
|
||||
_chipScale.ScaleX = 0.96;
|
||||
_chipScale.ScaleY = 0.96;
|
||||
|
||||
Dispatcher.UIThread.Post(
|
||||
() =>
|
||||
{
|
||||
if (version != _transitionVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_collapsedChipHost.Opacity = 1;
|
||||
_chipTranslate.Y = 0;
|
||||
_chipScale.ScaleX = 1;
|
||||
_chipScale.ScaleY = 1;
|
||||
},
|
||||
DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private void HideCollapsedChip(int version)
|
||||
{
|
||||
_collapsedChipHost.IsVisible = true;
|
||||
_collapsedChipHost.IsHitTestVisible = false;
|
||||
_collapsedChipHost.Opacity = 0;
|
||||
_chipTranslate.Y = 8;
|
||||
_chipScale.ScaleX = 0.96;
|
||||
_chipScale.ScaleY = 0.96;
|
||||
|
||||
DispatcherTimer.RunOnce(
|
||||
() =>
|
||||
{
|
||||
if (version != _transitionVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_collapsedChipHost.IsVisible = false;
|
||||
},
|
||||
TransitionDuration);
|
||||
}
|
||||
|
||||
private void PrepareRestoringWindow()
|
||||
{
|
||||
_componentLibraryWindow.IsVisible = true;
|
||||
_componentLibraryWindow.IsHitTestVisible = true;
|
||||
_componentLibraryWindow.Margin = _state.ExpandedMargin;
|
||||
_componentLibraryWindow.Opacity = 0;
|
||||
_windowTranslate.Y = 28;
|
||||
}
|
||||
|
||||
private static bool IsStableExpandedOpacity(double opacity)
|
||||
{
|
||||
return !double.IsNaN(opacity) &&
|
||||
!double.IsInfinity(opacity) &&
|
||||
opacity > StableOpacityThreshold;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Avalonia;
|
||||
|
||||
namespace LanMountainDesktop.DesktopEditing;
|
||||
|
||||
internal enum ComponentLibraryCollapseVisualState
|
||||
{
|
||||
Expanded,
|
||||
Collapsing,
|
||||
Collapsed,
|
||||
Restoring
|
||||
}
|
||||
|
||||
internal readonly record struct ComponentLibraryCollapseState(
|
||||
ComponentLibraryCollapseVisualState VisualState,
|
||||
Thickness ExpandedMargin,
|
||||
double ExpandedOpacity,
|
||||
bool IsChipVisible)
|
||||
{
|
||||
public static ComponentLibraryCollapseState CreateExpanded(Thickness expandedMargin, double expandedOpacity)
|
||||
{
|
||||
return new(
|
||||
ComponentLibraryCollapseVisualState.Expanded,
|
||||
expandedMargin,
|
||||
expandedOpacity,
|
||||
IsChipVisible: false);
|
||||
}
|
||||
|
||||
public ComponentLibraryCollapseState WithVisualState(ComponentLibraryCollapseVisualState visualState, bool isChipVisible)
|
||||
{
|
||||
return this with
|
||||
{
|
||||
VisualState = visualState,
|
||||
IsChipVisible = isChipVisible
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace LanMountainDesktop.DesktopEditing;
|
||||
|
||||
internal static class DesktopEditCommitMath
|
||||
{
|
||||
public static bool IsPendingCommitValid(bool isPending, int scheduledVersion, int currentVersion)
|
||||
{
|
||||
return isPending && scheduledVersion == currentVersion;
|
||||
}
|
||||
}
|
||||
358
LanMountainDesktop/DesktopEditing/DesktopEditGhostView.cs
Normal file
358
LanMountainDesktop/DesktopEditing/DesktopEditGhostView.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
using Avalonia.Animation.Easings;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace LanMountainDesktop.DesktopEditing;
|
||||
|
||||
internal sealed class DesktopEditGhostView : Border
|
||||
{
|
||||
private static readonly TimeSpan FastDuration = TimeSpan.FromMilliseconds(120);
|
||||
private static readonly Easing StandardEasing = new CubicEaseOut();
|
||||
|
||||
private readonly Image _previewImage;
|
||||
private readonly Border _previewOverlay;
|
||||
private readonly Border _fallbackCard;
|
||||
private readonly Border _accentDot;
|
||||
private readonly TextBlock _titleTextBlock;
|
||||
private readonly TextBlock _detailTextBlock;
|
||||
private readonly Border _badgeBorder;
|
||||
private readonly TextBlock _badgeTextBlock;
|
||||
private readonly ScaleTransform _scaleTransform = new(1, 1);
|
||||
|
||||
private readonly SolidColorBrush _normalBackgroundBrush = new(Color.Parse("#F11B2430"));
|
||||
private readonly SolidColorBrush _normalBorderBrush = new(Color.Parse("#4D8AA3C1"));
|
||||
private readonly SolidColorBrush _normalAccentBrush = new(Color.Parse("#FF4F8EF7"));
|
||||
private readonly SolidColorBrush _normalTextBrush = new(Color.Parse("#FFF5F7FA"));
|
||||
private readonly SolidColorBrush _normalMutedTextBrush = new(Color.Parse("#BDE2E8F0"));
|
||||
private readonly SolidColorBrush _normalBadgeBackgroundBrush = new(Color.Parse("#245E86D6"));
|
||||
private readonly SolidColorBrush _normalBadgeBorderBrush = new(Color.Parse("#557EA7E6"));
|
||||
private readonly SolidColorBrush _invalidBackgroundBrush = new(Color.Parse("#F01B1022"));
|
||||
private readonly SolidColorBrush _invalidBorderBrush = new(Color.Parse("#FFE25555"));
|
||||
private readonly SolidColorBrush _invalidAccentBrush = new(Color.Parse("#FFFF6B6B"));
|
||||
private readonly SolidColorBrush _invalidBadgeBackgroundBrush = new(Color.Parse("#33FF4D4D"));
|
||||
private readonly SolidColorBrush _invalidBadgeBorderBrush = new(Color.Parse("#88FF7676"));
|
||||
|
||||
private bool _hasPreviewImage;
|
||||
private bool _isInvalid;
|
||||
|
||||
public DesktopEditGhostView()
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch;
|
||||
VerticalAlignment = VerticalAlignment.Stretch;
|
||||
Padding = new Thickness(14);
|
||||
Background = _normalBackgroundBrush;
|
||||
BorderBrush = _normalBorderBrush;
|
||||
BorderThickness = new Thickness(1);
|
||||
CornerRadius = new CornerRadius(22);
|
||||
ClipToBounds = true;
|
||||
RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
|
||||
RenderTransform = _scaleTransform;
|
||||
Transitions = new Transitions
|
||||
{
|
||||
CreateOpacityTransition(FastDuration)
|
||||
};
|
||||
_scaleTransform.Transitions = new Transitions
|
||||
{
|
||||
CreateScaleTransition(ScaleTransform.ScaleXProperty, FastDuration),
|
||||
CreateScaleTransition(ScaleTransform.ScaleYProperty, FastDuration)
|
||||
};
|
||||
|
||||
_accentDot = new Border
|
||||
{
|
||||
Width = 10,
|
||||
Height = 10,
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Background = _normalAccentBrush,
|
||||
BorderThickness = new Thickness(0),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
_titleTextBlock = new TextBlock
|
||||
{
|
||||
Foreground = _normalTextBrush,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
TextWrapping = TextWrapping.NoWrap,
|
||||
MaxLines = 1
|
||||
};
|
||||
|
||||
_detailTextBlock = new TextBlock
|
||||
{
|
||||
Foreground = _normalMutedTextBrush,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
TextWrapping = TextWrapping.NoWrap,
|
||||
MaxLines = 1
|
||||
};
|
||||
|
||||
_badgeTextBlock = new TextBlock
|
||||
{
|
||||
Foreground = _normalTextBrush,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
TextWrapping = TextWrapping.NoWrap,
|
||||
MaxLines = 1
|
||||
};
|
||||
|
||||
_badgeBorder = new Border
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
Padding = new Thickness(9, 4),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Background = _normalBadgeBackgroundBrush,
|
||||
BorderBrush = _normalBadgeBorderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Child = _badgeTextBlock
|
||||
};
|
||||
|
||||
_previewImage = new Image
|
||||
{
|
||||
Stretch = Stretch.UniformToFill,
|
||||
IsVisible = false
|
||||
};
|
||||
|
||||
_previewOverlay = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.Parse("#1A000000")),
|
||||
IsVisible = false
|
||||
};
|
||||
|
||||
var headerPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Spacing = 8,
|
||||
Children =
|
||||
{
|
||||
_accentDot,
|
||||
_titleTextBlock
|
||||
}
|
||||
};
|
||||
|
||||
var contentPanel = new StackPanel
|
||||
{
|
||||
Spacing = 6,
|
||||
Children =
|
||||
{
|
||||
headerPanel,
|
||||
_detailTextBlock
|
||||
}
|
||||
};
|
||||
|
||||
var fallbackGrid = new Grid
|
||||
{
|
||||
RowDefinitions = new RowDefinitions
|
||||
{
|
||||
new RowDefinition(GridLength.Auto),
|
||||
new RowDefinition(GridLength.Auto)
|
||||
},
|
||||
RowSpacing = 8
|
||||
};
|
||||
fallbackGrid.Children.Add(contentPanel);
|
||||
fallbackGrid.Children.Add(_badgeBorder);
|
||||
Grid.SetRow(contentPanel, 0);
|
||||
Grid.SetRow(_badgeBorder, 1);
|
||||
_badgeBorder.Margin = new Thickness(0, 2, 0, 0);
|
||||
|
||||
_fallbackCard = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
Child = fallbackGrid
|
||||
};
|
||||
|
||||
Child = new Grid
|
||||
{
|
||||
Children =
|
||||
{
|
||||
_previewImage,
|
||||
_previewOverlay,
|
||||
_fallbackCard
|
||||
}
|
||||
};
|
||||
|
||||
UpdatePreviewMetrics(180, 120);
|
||||
UpdateContent(null, null, null);
|
||||
ApplyShellChrome();
|
||||
}
|
||||
|
||||
public void UpdateContent(string? title, string? detail, string? badgeText)
|
||||
{
|
||||
_titleTextBlock.Text = string.IsNullOrWhiteSpace(title) ? "Component" : title;
|
||||
_detailTextBlock.Text = string.IsNullOrWhiteSpace(detail) ? string.Empty : detail;
|
||||
_detailTextBlock.IsVisible = !string.IsNullOrWhiteSpace(detail);
|
||||
_badgeTextBlock.Text = string.IsNullOrWhiteSpace(badgeText) ? string.Empty : badgeText;
|
||||
_badgeBorder.IsVisible = !string.IsNullOrWhiteSpace(badgeText);
|
||||
}
|
||||
|
||||
public void SetPreviewImage(IImage? image)
|
||||
{
|
||||
_previewImage.Source = image;
|
||||
_hasPreviewImage = image is not null;
|
||||
_previewImage.IsVisible = _hasPreviewImage;
|
||||
_previewOverlay.IsVisible = false;
|
||||
_fallbackCard.IsVisible = !_hasPreviewImage;
|
||||
ApplyShellChrome();
|
||||
}
|
||||
|
||||
public void UpdatePreviewMetrics(double width, double height)
|
||||
{
|
||||
var normalizedWidth = Math.Max(1, width);
|
||||
var normalizedHeight = Math.Max(1, height);
|
||||
var minSide = Math.Max(1, Math.Min(normalizedWidth, normalizedHeight));
|
||||
|
||||
CornerRadius = _hasPreviewImage
|
||||
? new CornerRadius(Math.Clamp(minSide * 0.14, 14, 24))
|
||||
: new CornerRadius(Math.Clamp(minSide * 0.16, 16, 28));
|
||||
Padding = _hasPreviewImage
|
||||
? new Thickness(
|
||||
Math.Clamp(minSide * 0.02, 1, 4),
|
||||
Math.Clamp(minSide * 0.02, 1, 4),
|
||||
Math.Clamp(minSide * 0.02, 1, 4),
|
||||
Math.Clamp(minSide * 0.02, 1, 4))
|
||||
: new Thickness(
|
||||
Math.Clamp(minSide * 0.10, 10, 18),
|
||||
Math.Clamp(minSide * 0.10, 10, 18),
|
||||
Math.Clamp(minSide * 0.10, 10, 18),
|
||||
Math.Clamp(minSide * 0.09, 10, 16));
|
||||
|
||||
var titleFontSize = Math.Clamp(minSide * 0.12, 12, 18);
|
||||
var detailFontSize = Math.Clamp(minSide * 0.085, 10, 13);
|
||||
var badgeFontSize = Math.Clamp(minSide * 0.08, 9, 12);
|
||||
var dotSize = Math.Clamp(minSide * 0.07, 8, 12);
|
||||
var badgeHorizontalPadding = Math.Clamp(minSide * 0.07, 8, 14);
|
||||
var badgeVerticalPadding = Math.Clamp(minSide * 0.035, 3, 6);
|
||||
|
||||
_accentDot.Width = dotSize;
|
||||
_accentDot.Height = dotSize;
|
||||
_titleTextBlock.FontSize = titleFontSize;
|
||||
_detailTextBlock.FontSize = detailFontSize;
|
||||
_badgeTextBlock.FontSize = badgeFontSize;
|
||||
_badgeBorder.Padding = new Thickness(badgeHorizontalPadding, badgeVerticalPadding);
|
||||
}
|
||||
|
||||
public void SetInvalid(bool isInvalid)
|
||||
{
|
||||
_isInvalid = isInvalid;
|
||||
|
||||
if (isInvalid)
|
||||
{
|
||||
_accentDot.Background = _invalidAccentBrush;
|
||||
_badgeBorder.Background = _invalidBadgeBackgroundBrush;
|
||||
_badgeBorder.BorderBrush = _invalidBadgeBorderBrush;
|
||||
_titleTextBlock.Foreground = _invalidBorderBrush;
|
||||
_detailTextBlock.Foreground = _invalidBorderBrush;
|
||||
_badgeTextBlock.Foreground = _invalidBorderBrush;
|
||||
if (!_hasPreviewImage)
|
||||
{
|
||||
Background = _invalidBackgroundBrush;
|
||||
BorderBrush = _invalidBorderBrush;
|
||||
BorderThickness = new Thickness(1);
|
||||
Opacity = 0.9;
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyShellChrome();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_accentDot.Background = _normalAccentBrush;
|
||||
_badgeBorder.Background = _normalBadgeBackgroundBrush;
|
||||
_badgeBorder.BorderBrush = _normalBadgeBorderBrush;
|
||||
_titleTextBlock.Foreground = _normalTextBrush;
|
||||
_detailTextBlock.Foreground = _normalMutedTextBrush;
|
||||
_badgeTextBlock.Foreground = _normalTextBrush;
|
||||
if (!_hasPreviewImage)
|
||||
{
|
||||
Background = _normalBackgroundBrush;
|
||||
BorderBrush = _normalBorderBrush;
|
||||
BorderThickness = new Thickness(1);
|
||||
Opacity = 1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyShellChrome();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetRestingScale(double scale)
|
||||
{
|
||||
var clampedScale = Math.Clamp(scale, 0.85, 1.12);
|
||||
_scaleTransform.ScaleX = clampedScale;
|
||||
_scaleTransform.ScaleY = clampedScale;
|
||||
}
|
||||
|
||||
public void AnimateToScale(double scale)
|
||||
{
|
||||
var clampedScale = Math.Clamp(scale, 0.85, 1.12);
|
||||
_scaleTransform.ScaleX = clampedScale;
|
||||
_scaleTransform.ScaleY = clampedScale;
|
||||
}
|
||||
|
||||
internal bool HasPreviewImage => _hasPreviewImage;
|
||||
|
||||
internal void SetScaleTransitionDuration(TimeSpan duration)
|
||||
{
|
||||
_scaleTransform.Transitions = new Transitions
|
||||
{
|
||||
CreateScaleTransition(ScaleTransform.ScaleXProperty, duration),
|
||||
CreateScaleTransition(ScaleTransform.ScaleYProperty, duration)
|
||||
};
|
||||
}
|
||||
|
||||
internal void SetOpacityTransitionDuration(TimeSpan duration)
|
||||
{
|
||||
Transitions = new Transitions
|
||||
{
|
||||
CreateOpacityTransition(duration)
|
||||
};
|
||||
}
|
||||
|
||||
private void ApplyShellChrome()
|
||||
{
|
||||
if (_hasPreviewImage)
|
||||
{
|
||||
Background = Brushes.Transparent;
|
||||
BorderBrush = Brushes.Transparent;
|
||||
BorderThickness = new Thickness(0);
|
||||
BoxShadow = BoxShadows.Parse("0 14 32 #1A000000");
|
||||
Opacity = 1.0;
|
||||
return;
|
||||
}
|
||||
|
||||
BoxShadow = default;
|
||||
if (_isInvalid)
|
||||
{
|
||||
Background = _invalidBackgroundBrush;
|
||||
BorderBrush = _invalidBorderBrush;
|
||||
BorderThickness = new Thickness(1);
|
||||
Opacity = 0.9;
|
||||
return;
|
||||
}
|
||||
|
||||
Background = _normalBackgroundBrush;
|
||||
BorderBrush = _normalBorderBrush;
|
||||
BorderThickness = new Thickness(1);
|
||||
Opacity = 1.0;
|
||||
}
|
||||
|
||||
private static DoubleTransition CreateScaleTransition(AvaloniaProperty property, TimeSpan duration) =>
|
||||
new()
|
||||
{
|
||||
Property = property,
|
||||
Duration = duration,
|
||||
Easing = StandardEasing
|
||||
};
|
||||
|
||||
private static DoubleTransition CreateOpacityTransition(TimeSpan duration) =>
|
||||
new()
|
||||
{
|
||||
Property = Visual.OpacityProperty,
|
||||
Duration = duration,
|
||||
Easing = StandardEasing
|
||||
};
|
||||
}
|
||||
343
LanMountainDesktop/DesktopEditing/DesktopEditOverlayPresenter.cs
Normal file
343
LanMountainDesktop/DesktopEditing/DesktopEditOverlayPresenter.cs
Normal file
@@ -0,0 +1,343 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
using Avalonia.Animation.Easings;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Theme;
|
||||
|
||||
namespace LanMountainDesktop.DesktopEditing;
|
||||
|
||||
internal enum DesktopEditGhostVisualStyle
|
||||
{
|
||||
StandardLift = 0,
|
||||
ElevatedFromLibrary
|
||||
}
|
||||
|
||||
internal sealed class DesktopEditOverlayPresenter
|
||||
{
|
||||
private static readonly TimeSpan FastDuration = FluttermotionToken.Fast;
|
||||
private static readonly TimeSpan PickupDuration = TimeSpan.FromMilliseconds(160);
|
||||
private static readonly TimeSpan CommitSettleDuration = TimeSpan.FromMilliseconds(160);
|
||||
private static readonly TimeSpan CancelSettleDuration = TimeSpan.FromMilliseconds(120);
|
||||
private static readonly Easing StandardEasing = new CubicEaseOut();
|
||||
|
||||
private readonly Canvas _root;
|
||||
private readonly DesktopEditGhostView _ghostView;
|
||||
private readonly Border _candidateOutline;
|
||||
private readonly ScaleTransform _candidateScale = new(1, 1);
|
||||
|
||||
private Rect? _previewRect;
|
||||
private Rect? _candidateRect;
|
||||
private bool _isInvalid;
|
||||
private bool _isVisible;
|
||||
private int _dismissVersion;
|
||||
|
||||
private readonly SolidColorBrush _candidateBrush = new(Color.Parse("#FF0A84FF"));
|
||||
private readonly SolidColorBrush _candidateInvalidBrush = new(Color.Parse("#FFFF3B30"));
|
||||
private readonly SolidColorBrush _candidateFillBrush = new(Color.Parse("#140A84FF"));
|
||||
private readonly SolidColorBrush _candidateInvalidFillBrush = new(Color.Parse("#14FF3B30"));
|
||||
|
||||
public DesktopEditOverlayPresenter()
|
||||
{
|
||||
_ghostView = new DesktopEditGhostView
|
||||
{
|
||||
IsHitTestVisible = false,
|
||||
Opacity = 1
|
||||
};
|
||||
|
||||
_candidateOutline = new Border
|
||||
{
|
||||
IsHitTestVisible = false,
|
||||
Background = _candidateFillBrush,
|
||||
BorderBrush = _candidateBrush,
|
||||
BorderThickness = new Thickness(2),
|
||||
CornerRadius = new CornerRadius(22),
|
||||
Opacity = 0,
|
||||
RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative),
|
||||
RenderTransform = _candidateScale,
|
||||
Transitions = new Transitions
|
||||
{
|
||||
new DoubleTransition
|
||||
{
|
||||
Property = Visual.OpacityProperty,
|
||||
Duration = FastDuration,
|
||||
Easing = StandardEasing
|
||||
}
|
||||
}
|
||||
};
|
||||
_candidateScale.Transitions = new Transitions
|
||||
{
|
||||
CreateScaleTransition(ScaleTransform.ScaleXProperty, FastDuration),
|
||||
CreateScaleTransition(ScaleTransform.ScaleYProperty, FastDuration)
|
||||
};
|
||||
|
||||
_candidateOutline.SetValue(Panel.ZIndexProperty, 0);
|
||||
_ghostView.SetValue(Panel.ZIndexProperty, 1);
|
||||
|
||||
_root = new Canvas
|
||||
{
|
||||
IsHitTestVisible = false,
|
||||
ClipToBounds = false,
|
||||
Opacity = 0,
|
||||
IsVisible = false,
|
||||
Children =
|
||||
{
|
||||
_candidateOutline,
|
||||
_ghostView
|
||||
}
|
||||
};
|
||||
|
||||
_root.Transitions = new Transitions
|
||||
{
|
||||
CreateOpacityTransition(FastDuration)
|
||||
};
|
||||
}
|
||||
|
||||
public Control Root => _root;
|
||||
|
||||
public void SetViewportSize(Size size)
|
||||
{
|
||||
_root.Width = Math.Max(1, size.Width);
|
||||
_root.Height = Math.Max(1, size.Height);
|
||||
}
|
||||
|
||||
public void SetPreviewRect(Rect rect)
|
||||
{
|
||||
_previewRect = Normalize(rect);
|
||||
ApplyPreviewRect();
|
||||
}
|
||||
|
||||
public void SetCandidateRect(Rect? rect)
|
||||
{
|
||||
_candidateRect = rect is null ? null : Normalize(rect.Value);
|
||||
ApplyCandidateRect();
|
||||
}
|
||||
|
||||
public void UpdateGhostContent(string? title, string? detail = null, string? badge = null)
|
||||
{
|
||||
_ghostView.UpdateContent(title, detail, badge);
|
||||
}
|
||||
|
||||
public void SetPreviewImage(IImage? image)
|
||||
{
|
||||
_ghostView.SetPreviewImage(image);
|
||||
}
|
||||
|
||||
public void SetInvalid(bool isInvalid)
|
||||
{
|
||||
_isInvalid = isInvalid;
|
||||
_ghostView.SetInvalid(isInvalid);
|
||||
UpdateCandidateAppearance();
|
||||
}
|
||||
|
||||
public void Show(DesktopEditGhostVisualStyle visualStyle = DesktopEditGhostVisualStyle.StandardLift)
|
||||
{
|
||||
_dismissVersion++;
|
||||
_isVisible = true;
|
||||
_root.IsVisible = true;
|
||||
_root.Opacity = 0;
|
||||
_ghostView.Opacity = 0;
|
||||
var imageMode = _ghostView.HasPreviewImage;
|
||||
var initialGhostScale = 0.985;
|
||||
var targetGhostScale = 1.0;
|
||||
|
||||
if (visualStyle == DesktopEditGhostVisualStyle.ElevatedFromLibrary)
|
||||
{
|
||||
initialGhostScale = 1.02;
|
||||
targetGhostScale = 1.06;
|
||||
}
|
||||
else if (imageMode)
|
||||
{
|
||||
initialGhostScale = 0.992;
|
||||
targetGhostScale = 1.03;
|
||||
}
|
||||
|
||||
_root.Transitions = new Transitions
|
||||
{
|
||||
CreateOpacityTransition(PickupDuration)
|
||||
};
|
||||
_ghostView.SetOpacityTransitionDuration(PickupDuration);
|
||||
_ghostView.SetScaleTransitionDuration(PickupDuration);
|
||||
_candidateScale.Transitions = new Transitions
|
||||
{
|
||||
CreateScaleTransition(ScaleTransform.ScaleXProperty, PickupDuration),
|
||||
CreateScaleTransition(ScaleTransform.ScaleYProperty, PickupDuration)
|
||||
};
|
||||
_candidateOutline.Transitions = new Transitions
|
||||
{
|
||||
CreateOpacityTransition(PickupDuration)
|
||||
};
|
||||
_ghostView.SetRestingScale(initialGhostScale);
|
||||
_candidateOutline.Opacity = 0;
|
||||
_candidateScale.ScaleX = 0.97;
|
||||
_candidateScale.ScaleY = 0.97;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (!_isVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_root.Opacity = 1;
|
||||
_ghostView.Opacity = 1;
|
||||
_ghostView.SetRestingScale(targetGhostScale);
|
||||
if (_candidateRect.HasValue)
|
||||
{
|
||||
_candidateOutline.Opacity = 1;
|
||||
_candidateScale.ScaleX = 1;
|
||||
_candidateScale.ScaleY = 1;
|
||||
}
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
_dismissVersion++;
|
||||
_isVisible = false;
|
||||
_root.Opacity = 0;
|
||||
_ghostView.Opacity = 0;
|
||||
_candidateOutline.Opacity = 0;
|
||||
_candidateScale.ScaleX = 0.96;
|
||||
_candidateScale.ScaleY = 0.96;
|
||||
_ghostView.SetRestingScale(0.96);
|
||||
_ghostView.SetPreviewImage(null);
|
||||
_root.IsVisible = false;
|
||||
}
|
||||
|
||||
public void Commit()
|
||||
{
|
||||
BeginDismiss(isCancel: false);
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
BeginDismiss(isCancel: true);
|
||||
}
|
||||
|
||||
private void BeginDismiss(bool isCancel)
|
||||
{
|
||||
if (!_isVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var version = ++_dismissVersion;
|
||||
_isVisible = false;
|
||||
var settleDuration = isCancel ? CancelSettleDuration : CommitSettleDuration;
|
||||
_root.Transitions = new Transitions
|
||||
{
|
||||
CreateOpacityTransition(settleDuration)
|
||||
};
|
||||
_ghostView.SetOpacityTransitionDuration(settleDuration);
|
||||
_ghostView.SetScaleTransitionDuration(settleDuration);
|
||||
_candidateScale.Transitions = new Transitions
|
||||
{
|
||||
CreateScaleTransition(ScaleTransform.ScaleXProperty, settleDuration),
|
||||
CreateScaleTransition(ScaleTransform.ScaleYProperty, settleDuration)
|
||||
};
|
||||
_candidateOutline.Transitions = new Transitions
|
||||
{
|
||||
CreateOpacityTransition(settleDuration)
|
||||
};
|
||||
var targetScale = _ghostView.HasPreviewImage
|
||||
? 1.00
|
||||
: isCancel ? 0.96 : 1.04;
|
||||
|
||||
_candidateOutline.Opacity = 0;
|
||||
_ghostView.Opacity = 0;
|
||||
_root.Opacity = 0;
|
||||
_ghostView.AnimateToScale(targetScale);
|
||||
_candidateScale.ScaleX = targetScale;
|
||||
_candidateScale.ScaleY = targetScale;
|
||||
|
||||
DispatcherTimer.RunOnce(
|
||||
() =>
|
||||
{
|
||||
if (version != _dismissVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_root.IsVisible = false;
|
||||
},
|
||||
FastDuration);
|
||||
}
|
||||
|
||||
private void ApplyPreviewRect()
|
||||
{
|
||||
if (!_previewRect.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var rect = _previewRect.Value;
|
||||
_ghostView.Width = Math.Max(1, rect.Width);
|
||||
_ghostView.Height = Math.Max(1, rect.Height);
|
||||
Canvas.SetLeft(_ghostView, rect.X);
|
||||
Canvas.SetTop(_ghostView, rect.Y);
|
||||
_ghostView.UpdatePreviewMetrics(rect.Width, rect.Height);
|
||||
}
|
||||
|
||||
private void ApplyCandidateRect()
|
||||
{
|
||||
if (!_candidateRect.HasValue)
|
||||
{
|
||||
_candidateOutline.IsVisible = false;
|
||||
_candidateOutline.Opacity = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var rect = _candidateRect.Value;
|
||||
_candidateOutline.IsVisible = true;
|
||||
_candidateOutline.Width = Math.Max(1, rect.Width);
|
||||
_candidateOutline.Height = Math.Max(1, rect.Height);
|
||||
Canvas.SetLeft(_candidateOutline, rect.X);
|
||||
Canvas.SetTop(_candidateOutline, rect.Y);
|
||||
|
||||
var cornerRadius = Math.Clamp(Math.Min(rect.Width, rect.Height) * 0.11, 14, 26);
|
||||
_candidateOutline.CornerRadius = new CornerRadius(cornerRadius);
|
||||
_candidateOutline.BorderBrush = _isInvalid ? _candidateInvalidBrush : _candidateBrush;
|
||||
_candidateOutline.Background = _isInvalid ? _candidateInvalidFillBrush : _candidateFillBrush;
|
||||
_candidateOutline.Opacity = _isVisible ? 1 : 0;
|
||||
_candidateScale.ScaleX = _isVisible ? 1 : 0.97;
|
||||
_candidateScale.ScaleY = _isVisible ? 1 : 0.97;
|
||||
UpdateCandidateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateCandidateAppearance()
|
||||
{
|
||||
if (!_candidateRect.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_candidateOutline.BorderBrush = _isInvalid ? _candidateInvalidBrush : _candidateBrush;
|
||||
_candidateOutline.Background = _isInvalid ? _candidateInvalidFillBrush : _candidateFillBrush;
|
||||
}
|
||||
|
||||
private static Rect Normalize(Rect rect)
|
||||
{
|
||||
var width = Math.Max(1, rect.Width);
|
||||
var height = Math.Max(1, rect.Height);
|
||||
return new Rect(rect.X, rect.Y, width, height);
|
||||
}
|
||||
|
||||
private static DoubleTransition CreateScaleTransition(AvaloniaProperty property, TimeSpan duration) =>
|
||||
new()
|
||||
{
|
||||
Property = property,
|
||||
Duration = duration,
|
||||
Easing = StandardEasing
|
||||
};
|
||||
|
||||
private static DoubleTransition CreateOpacityTransition(TimeSpan duration) =>
|
||||
new()
|
||||
{
|
||||
Property = Visual.OpacityProperty,
|
||||
Duration = duration,
|
||||
Easing = StandardEasing
|
||||
};
|
||||
}
|
||||
205
LanMountainDesktop/DesktopEditing/DesktopEditSession.cs
Normal file
205
LanMountainDesktop/DesktopEditing/DesktopEditSession.cs
Normal file
@@ -0,0 +1,205 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
|
||||
namespace LanMountainDesktop.DesktopEditing;
|
||||
|
||||
internal enum DesktopEditSessionMode
|
||||
{
|
||||
None = 0,
|
||||
PendingNew,
|
||||
DraggingNew,
|
||||
DraggingExisting,
|
||||
ResizingExisting
|
||||
}
|
||||
|
||||
internal readonly record struct DesktopEditSession
|
||||
{
|
||||
public DesktopEditSessionMode Mode { get; init; }
|
||||
public string? ComponentId { get; init; }
|
||||
public string? PlacementId { get; init; }
|
||||
public int PageIndex { get; init; }
|
||||
public int WidthCells { get; init; }
|
||||
public int HeightCells { get; init; }
|
||||
public Point StartPointerInViewport { get; init; }
|
||||
public Point CurrentPointerInViewport { get; init; }
|
||||
public Point PointerOffsetInViewport { get; init; }
|
||||
public Rect? ComponentLibraryBounds { get; init; }
|
||||
public int TargetRow { get; init; }
|
||||
public int TargetColumn { get; init; }
|
||||
|
||||
public bool IsActive => Mode != DesktopEditSessionMode.None;
|
||||
public bool IsPendingNew => Mode == DesktopEditSessionMode.PendingNew;
|
||||
public bool IsDraggingNew => Mode == DesktopEditSessionMode.DraggingNew;
|
||||
public bool IsDraggingExisting => Mode == DesktopEditSessionMode.DraggingExisting;
|
||||
public bool IsResizingExisting => Mode == DesktopEditSessionMode.ResizingExisting;
|
||||
public bool HasTargetCell => TargetRow >= 0 && TargetColumn >= 0;
|
||||
|
||||
public double PointerTravelDistance => DesktopPlacementMath.Distance(StartPointerInViewport, CurrentPointerInViewport);
|
||||
|
||||
public bool HasExceededThreshold(double threshold)
|
||||
{
|
||||
return DesktopPlacementMath.HasExceededThreshold(StartPointerInViewport, CurrentPointerInViewport, threshold);
|
||||
}
|
||||
|
||||
public bool IsPointerInsideComponentLibrary()
|
||||
{
|
||||
return DesktopPlacementMath.IsOccludedByComponentLibrary(CurrentPointerInViewport, ComponentLibraryBounds);
|
||||
}
|
||||
|
||||
public bool IsPreviewOccludedByComponentLibrary(Rect previewRect)
|
||||
{
|
||||
return DesktopPlacementMath.IsOccludedByComponentLibrary(previewRect, ComponentLibraryBounds);
|
||||
}
|
||||
|
||||
public bool CanCommit => IsActive && HasTargetCell;
|
||||
|
||||
public Rect GetPreviewRect(DesktopGridGeometry grid)
|
||||
{
|
||||
if (HasTargetCell)
|
||||
{
|
||||
return DesktopPlacementMath.GetCellRect(
|
||||
grid,
|
||||
TargetColumn,
|
||||
TargetRow,
|
||||
Math.Max(1, WidthCells),
|
||||
Math.Max(1, HeightCells));
|
||||
}
|
||||
|
||||
var freePreviewOrigin = DesktopPlacementMath.Subtract(CurrentPointerInViewport, PointerOffsetInViewport);
|
||||
return new Rect(
|
||||
freePreviewOrigin,
|
||||
new Size(
|
||||
Math.Max(1, WidthCells) * grid.CellSize + Math.Max(0, Math.Max(1, WidthCells) - 1) * grid.CellGap,
|
||||
Math.Max(1, HeightCells) * grid.CellSize + Math.Max(0, Math.Max(1, HeightCells) - 1) * grid.CellGap));
|
||||
}
|
||||
|
||||
public DesktopEditSession WithCurrentPointer(Point pointerInViewport)
|
||||
{
|
||||
return this with { CurrentPointerInViewport = pointerInViewport };
|
||||
}
|
||||
|
||||
public DesktopEditSession WithComponentLibraryBounds(Rect? componentLibraryBounds)
|
||||
{
|
||||
return this with { ComponentLibraryBounds = componentLibraryBounds };
|
||||
}
|
||||
|
||||
public DesktopEditSession WithTargetCell(int row, int column)
|
||||
{
|
||||
return this with { TargetRow = row, TargetColumn = column };
|
||||
}
|
||||
|
||||
public DesktopEditSession PromoteToDraggingNew()
|
||||
{
|
||||
return this with { Mode = DesktopEditSessionMode.DraggingNew };
|
||||
}
|
||||
|
||||
public DesktopEditSession PromoteToDraggingExisting()
|
||||
{
|
||||
return this with { Mode = DesktopEditSessionMode.DraggingExisting };
|
||||
}
|
||||
|
||||
public DesktopEditSession PromoteToResizingExisting()
|
||||
{
|
||||
return this with { Mode = DesktopEditSessionMode.ResizingExisting };
|
||||
}
|
||||
|
||||
public static DesktopEditSession CreatePendingNew(
|
||||
string componentId,
|
||||
int pageIndex,
|
||||
int widthCells,
|
||||
int heightCells,
|
||||
Point startPointerInViewport,
|
||||
Point pointerOffsetInViewport,
|
||||
Rect? componentLibraryBounds)
|
||||
{
|
||||
return new DesktopEditSession
|
||||
{
|
||||
Mode = DesktopEditSessionMode.PendingNew,
|
||||
ComponentId = componentId,
|
||||
PageIndex = pageIndex,
|
||||
WidthCells = Math.Max(1, widthCells),
|
||||
HeightCells = Math.Max(1, heightCells),
|
||||
StartPointerInViewport = startPointerInViewport,
|
||||
CurrentPointerInViewport = startPointerInViewport,
|
||||
PointerOffsetInViewport = pointerOffsetInViewport,
|
||||
ComponentLibraryBounds = componentLibraryBounds,
|
||||
TargetRow = -1,
|
||||
TargetColumn = -1
|
||||
};
|
||||
}
|
||||
|
||||
public static DesktopEditSession CreateDraggingNew(
|
||||
string componentId,
|
||||
int pageIndex,
|
||||
int widthCells,
|
||||
int heightCells,
|
||||
Point startPointerInViewport,
|
||||
Point pointerOffsetInViewport,
|
||||
Rect? componentLibraryBounds)
|
||||
{
|
||||
return CreatePendingNew(
|
||||
componentId,
|
||||
pageIndex,
|
||||
widthCells,
|
||||
heightCells,
|
||||
startPointerInViewport,
|
||||
pointerOffsetInViewport,
|
||||
componentLibraryBounds) with
|
||||
{
|
||||
Mode = DesktopEditSessionMode.DraggingNew
|
||||
};
|
||||
}
|
||||
|
||||
public static DesktopEditSession CreateDraggingExisting(
|
||||
string componentId,
|
||||
string placementId,
|
||||
int pageIndex,
|
||||
int widthCells,
|
||||
int heightCells,
|
||||
Point startPointerInViewport,
|
||||
Point pointerOffsetInViewport,
|
||||
Rect? componentLibraryBounds)
|
||||
{
|
||||
return new DesktopEditSession
|
||||
{
|
||||
Mode = DesktopEditSessionMode.DraggingExisting,
|
||||
ComponentId = componentId,
|
||||
PlacementId = placementId,
|
||||
PageIndex = pageIndex,
|
||||
WidthCells = Math.Max(1, widthCells),
|
||||
HeightCells = Math.Max(1, heightCells),
|
||||
StartPointerInViewport = startPointerInViewport,
|
||||
CurrentPointerInViewport = startPointerInViewport,
|
||||
PointerOffsetInViewport = pointerOffsetInViewport,
|
||||
ComponentLibraryBounds = componentLibraryBounds,
|
||||
TargetRow = -1,
|
||||
TargetColumn = -1
|
||||
};
|
||||
}
|
||||
|
||||
public static DesktopEditSession CreateResizingExisting(
|
||||
string componentId,
|
||||
string placementId,
|
||||
int pageIndex,
|
||||
int widthCells,
|
||||
int heightCells,
|
||||
Point startPointerInViewport,
|
||||
Rect? componentLibraryBounds)
|
||||
{
|
||||
return new DesktopEditSession
|
||||
{
|
||||
Mode = DesktopEditSessionMode.ResizingExisting,
|
||||
ComponentId = componentId,
|
||||
PlacementId = placementId,
|
||||
PageIndex = pageIndex,
|
||||
WidthCells = Math.Max(1, widthCells),
|
||||
HeightCells = Math.Max(1, heightCells),
|
||||
StartPointerInViewport = startPointerInViewport,
|
||||
CurrentPointerInViewport = startPointerInViewport,
|
||||
PointerOffsetInViewport = default,
|
||||
ComponentLibraryBounds = componentLibraryBounds,
|
||||
TargetRow = -1,
|
||||
TargetColumn = -1
|
||||
};
|
||||
}
|
||||
}
|
||||
176
LanMountainDesktop/DesktopEditing/DesktopPlacementMath.cs
Normal file
176
LanMountainDesktop/DesktopEditing/DesktopPlacementMath.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
|
||||
namespace LanMountainDesktop.DesktopEditing;
|
||||
|
||||
internal readonly record struct DesktopGridGeometry(
|
||||
Point Origin,
|
||||
double CellSize,
|
||||
double CellGap,
|
||||
int ColumnCount,
|
||||
int RowCount)
|
||||
{
|
||||
public double Pitch => CellSize + CellGap;
|
||||
|
||||
public bool IsValid =>
|
||||
CellSize > 0 &&
|
||||
ColumnCount > 0 &&
|
||||
RowCount > 0 &&
|
||||
Pitch > 0;
|
||||
}
|
||||
|
||||
internal static class DesktopPlacementMath
|
||||
{
|
||||
public static double ComputeDragStartThreshold(double cellSize)
|
||||
{
|
||||
return Math.Max(10d, Math.Max(0d, cellSize) * 0.18d);
|
||||
}
|
||||
|
||||
public static double Distance(Point start, Point end)
|
||||
{
|
||||
return Math.Sqrt(DistanceSquared(start, end));
|
||||
}
|
||||
|
||||
public static double DistanceSquared(Point start, Point end)
|
||||
{
|
||||
var deltaX = end.X - start.X;
|
||||
var deltaY = end.Y - start.Y;
|
||||
return deltaX * deltaX + deltaY * deltaY;
|
||||
}
|
||||
|
||||
public static bool HasExceededThreshold(Point start, Point end, double threshold)
|
||||
{
|
||||
if (threshold <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return DistanceSquared(start, end) >= threshold * threshold;
|
||||
}
|
||||
|
||||
public static Point Add(Point left, Point right)
|
||||
{
|
||||
return new Point(left.X + right.X, left.Y + right.Y);
|
||||
}
|
||||
|
||||
public static Point Subtract(Point left, Point right)
|
||||
{
|
||||
return new Point(left.X - right.X, left.Y - right.Y);
|
||||
}
|
||||
|
||||
public static bool ContainsPoint(Rect rect, Point point)
|
||||
{
|
||||
return rect.Contains(point);
|
||||
}
|
||||
|
||||
public static bool Intersects(Rect left, Rect right)
|
||||
{
|
||||
return left.Intersects(right);
|
||||
}
|
||||
|
||||
public static bool HasCellPositionChanged(int originalRow, int originalColumn, int targetRow, int targetColumn)
|
||||
{
|
||||
return originalRow != targetRow || originalColumn != targetColumn;
|
||||
}
|
||||
|
||||
public static bool HasCellSpanChanged(int originalWidthCells, int originalHeightCells, int targetWidthCells, int targetHeightCells)
|
||||
{
|
||||
return originalWidthCells != targetWidthCells || originalHeightCells != targetHeightCells;
|
||||
}
|
||||
|
||||
public static bool IsOccludedByComponentLibrary(Point point, Rect? componentLibraryBounds)
|
||||
{
|
||||
return componentLibraryBounds.HasValue && ContainsPoint(componentLibraryBounds.Value, point);
|
||||
}
|
||||
|
||||
public static bool IsOccludedByComponentLibrary(Rect previewRect, Rect? componentLibraryBounds)
|
||||
{
|
||||
return componentLibraryBounds.HasValue && Intersects(previewRect, componentLibraryBounds.Value);
|
||||
}
|
||||
|
||||
public static bool CanCommitPlacement(Rect placementRect, Rect? componentLibraryBounds)
|
||||
{
|
||||
return !IsOccludedByComponentLibrary(placementRect, componentLibraryBounds);
|
||||
}
|
||||
|
||||
public static Rect GetGridBounds(DesktopGridGeometry grid)
|
||||
{
|
||||
if (!grid.IsValid)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var width = grid.ColumnCount * grid.CellSize + Math.Max(0, grid.ColumnCount - 1) * grid.CellGap;
|
||||
var height = grid.RowCount * grid.CellSize + Math.Max(0, grid.RowCount - 1) * grid.CellGap;
|
||||
return new Rect(grid.Origin, new Size(width, height));
|
||||
}
|
||||
|
||||
public static Rect GetCellRect(
|
||||
DesktopGridGeometry grid,
|
||||
int column,
|
||||
int row,
|
||||
int widthCells = 1,
|
||||
int heightCells = 1)
|
||||
{
|
||||
var safeWidthCells = Math.Max(1, widthCells);
|
||||
var safeHeightCells = Math.Max(1, heightCells);
|
||||
var safeColumn = Math.Max(0, column);
|
||||
var safeRow = Math.Max(0, row);
|
||||
var pitch = grid.Pitch;
|
||||
var x = grid.Origin.X + safeColumn * pitch;
|
||||
var y = grid.Origin.Y + safeRow * pitch;
|
||||
var width = safeWidthCells * grid.CellSize + Math.Max(0, safeWidthCells - 1) * grid.CellGap;
|
||||
var height = safeHeightCells * grid.CellSize + Math.Max(0, safeHeightCells - 1) * grid.CellGap;
|
||||
return new Rect(x, y, width, height);
|
||||
}
|
||||
|
||||
public static Rect GetSnappedCellRect(
|
||||
DesktopGridGeometry grid,
|
||||
Point pointerInViewport,
|
||||
Point pointerOffset,
|
||||
int widthCells,
|
||||
int heightCells)
|
||||
{
|
||||
return TryGetSnappedCell(grid, pointerInViewport, pointerOffset, widthCells, heightCells, out var column, out var row)
|
||||
? GetCellRect(grid, column, row, widthCells, heightCells)
|
||||
: default;
|
||||
}
|
||||
|
||||
public static bool TryGetSnappedCell(
|
||||
DesktopGridGeometry grid,
|
||||
Point pointerInViewport,
|
||||
Point pointerOffset,
|
||||
int widthCells,
|
||||
int heightCells,
|
||||
out int column,
|
||||
out int row)
|
||||
{
|
||||
column = 0;
|
||||
row = 0;
|
||||
|
||||
if (!grid.IsValid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var safeWidthCells = Math.Max(1, widthCells);
|
||||
var safeHeightCells = Math.Max(1, heightCells);
|
||||
var maxColumn = Math.Max(0, grid.ColumnCount - safeWidthCells);
|
||||
var maxRow = Math.Max(0, grid.RowCount - safeHeightCells);
|
||||
var pitch = grid.Pitch;
|
||||
if (pitch <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var previewOrigin = Subtract(pointerInViewport, pointerOffset);
|
||||
var relativeX = previewOrigin.X - grid.Origin.X;
|
||||
var relativeY = previewOrigin.Y - grid.Origin.Y;
|
||||
|
||||
column = (int)Math.Floor(relativeX / pitch);
|
||||
row = (int)Math.Floor(relativeY / pitch);
|
||||
column = Math.Clamp(column, 0, maxColumn);
|
||||
row = Math.Clamp(row, 0, maxRow);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using Markdown.Avalonia;
|
||||
|
||||
namespace LanMountainDesktop.Helpers;
|
||||
|
||||
public static class PluginMarketMarkdownHelper
|
||||
public static class PluginCatalogMarkdownHelper
|
||||
{
|
||||
private static Markdown.Avalonia.Markdown? _engine;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
@@ -21,11 +21,20 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="Models\" />
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
<AvaloniaResource Include="Localization\**" />
|
||||
<EmbeddedResource Include="Assets\Documents\Privacy.md" />
|
||||
<EmbeddedResource Include="Localization\*.json" />
|
||||
<None Include="Localization\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="Extensions\Components\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LanMountainDesktop.Host.Abstractions\LanMountainDesktop.Host.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Settings.Core\LanMountainDesktop.Settings.Core.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Appearance\LanMountainDesktop.Appearance.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.DesktopComponents.Runtime\LanMountainDesktop.DesktopComponents.Runtime.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.DesktopHost\LanMountainDesktop.DesktopHost.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.PluginsInstallHelper\LanMountainDesktop.PluginsInstallHelper.csproj" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
@@ -52,15 +61,18 @@
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" />
|
||||
<PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.5" />
|
||||
<PackageReference Include="MudTools.OfficeInterop" Version="2.0.8" />
|
||||
<PackageReference Include="MudTools.OfficeInterop.Word" Version="2.0.8" />
|
||||
<PackageReference Include="MudTools.OfficeInterop.Excel" Version="2.0.8" />
|
||||
<PackageReference Include="MudTools.OfficeInterop.PowerPoint" Version="2.0.8" />
|
||||
|
||||
<PackageReference Include="PortAudioSharp2" Version="1.0.6" />
|
||||
<PackageReference Include="MaterialColorUtilities" Version="0.3.0" />
|
||||
<PackageReference Include="PostHog" Version="2.4.0" />
|
||||
<PackageReference Include="Sentry" Version="4.0.0" />
|
||||
<PackageReference Include="System.Runtime.WindowsRuntime" Version="4.7.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
|
||||
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.23" Condition="('$(RuntimeIdentifier)' == '' and $([MSBuild]::IsOSPlatform('Windows')))
 or '$(RuntimeIdentifier)' == 'win-x64'
 or '$(RuntimeIdentifier)' == 'win-x86'" />
|
||||
<PackageReference Include="VideoLAN.LibVLC.Mac" Version="3.1.3.1" Condition="('$(RuntimeIdentifier)' == '' and $([MSBuild]::IsOSPlatform('OSX')))
 or '$(RuntimeIdentifier)' == 'osx-x64'" />
|
||||
|
||||
<PackageReference Include="WebView.Avalonia" Version="11.0.0.1" />
|
||||
<PackageReference Include="WebView.Avalonia.Desktop" Version="11.0.0.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
"tray.menu.restart": "Restart App",
|
||||
"tray.menu.exit": "Exit App",
|
||||
"button.back_to_windows": "Back to Windows",
|
||||
"button.back_to_platform": "Back to {0}",
|
||||
"tooltip.back_to_windows": "Back to Windows",
|
||||
"tooltip.back_to_platform": "Back to {0}",
|
||||
"platform.windows": "Windows",
|
||||
"platform.linux": "Linux",
|
||||
"platform.macos": "macOS",
|
||||
"tooltip.open_settings": "Settings",
|
||||
"settings.title": "Settings",
|
||||
"settings.shell.title": "Settings",
|
||||
@@ -20,7 +25,7 @@
|
||||
"settings.nav.group_system": "System",
|
||||
"settings.nav.group_extensions": "Extensions",
|
||||
"settings.nav.wallpaper": "Wallpaper",
|
||||
"settings.nav.grid": "Grid",
|
||||
"settings.nav.grid": "Components",
|
||||
"settings.nav.color": "Color",
|
||||
"settings.nav.status_bar": "Status Bar",
|
||||
"settings.nav.weather": "Weather",
|
||||
@@ -33,6 +38,27 @@
|
||||
"settings.wallpaper.title": "Wallpaper",
|
||||
"settings.wallpaper.description": "Pick an image or video to apply as the app window wallpaper immediately.",
|
||||
"settings.wallpaper.current_label": "Current Wallpaper",
|
||||
"settings.wallpaper.type_label": "Wallpaper Type",
|
||||
"settings.wallpaper.type.image": "Image",
|
||||
"settings.wallpaper.type.solid_color": "Solid Color",
|
||||
"settings.wallpaper.type.system": "System Wallpaper",
|
||||
"settings.wallpaper.system.label": "System Wallpaper",
|
||||
"settings.wallpaper.system.unavailable": "Unable to read system wallpaper",
|
||||
"settings.wallpaper.refresh_interval": "Refresh Interval",
|
||||
"settings.wallpaper.refresh_now": "Refresh Now",
|
||||
"settings.wallpaper.refresh.30s": "30 seconds",
|
||||
"settings.wallpaper.refresh.1m": "1 minute",
|
||||
"settings.wallpaper.refresh.5m": "5 minutes",
|
||||
"settings.wallpaper.refresh.10m": "10 minutes",
|
||||
"settings.wallpaper.refresh.15m": "15 minutes",
|
||||
"settings.wallpaper.refresh.30m": "30 minutes",
|
||||
"settings.wallpaper.refresh.1h": "1 hour",
|
||||
"settings.wallpaper.refresh.2h": "2 hours",
|
||||
"settings.wallpaper.refresh.4h": "4 hours",
|
||||
"settings.wallpaper.refresh.8h": "8 hours",
|
||||
"settings.wallpaper.refresh.12h": "12 hours",
|
||||
"settings.wallpaper.refresh.24h": "24 hours",
|
||||
"settings.wallpaper.color_label": "Wallpaper Color",
|
||||
"settings.wallpaper.placement_label": "Placement",
|
||||
"settings.wallpaper.placement_desc": "Adjust how the image fills the desktop.",
|
||||
"settings.wallpaper.pick_button": "Browse Files",
|
||||
@@ -86,6 +112,8 @@
|
||||
"settings.status_bar.description": "Choose which components appear on the top status bar.",
|
||||
"settings.status_bar.clock_header": "Clock Component",
|
||||
"settings.status_bar.clock_description": "Display a clock on the top status bar.",
|
||||
"settings.status_bar.clock_transparent_background_label": "Transparent background",
|
||||
"settings.status_bar.clock_transparent_background_desc": "Remove the capsule background and keep only the clock text.",
|
||||
"settings.status_bar.spacing_header": "Component Spacing",
|
||||
"settings.status_bar.spacing_desc": "Adjust spacing between status bar components.",
|
||||
"settings.status_bar.spacing_mode_compact": "Compact",
|
||||
@@ -99,6 +127,11 @@
|
||||
"settings.privacy.crash_upload_description": "Help us improve application stability.",
|
||||
"settings.privacy.usage_upload_title": "Anonymous usage data uploads",
|
||||
"settings.privacy.usage_upload_description": "Help us improve application features.",
|
||||
"settings.privacy.device_id_title": "Device ID",
|
||||
"settings.privacy.device_id_description": "Unique identifier for this device. Click refresh to regenerate.",
|
||||
"settings.privacy.refresh_device_id": "Refresh",
|
||||
"settings.privacy.policy_hint_prefix": "For more details, please ",
|
||||
"settings.privacy.view_policy": "view our privacy policy",
|
||||
"settings.weather.title": "Weather",
|
||||
"settings.weather.description": "Configure weather location, Xiaomi weather preview, and startup positioning behavior.",
|
||||
"settings.weather.location_source_header": "Location Source",
|
||||
@@ -205,7 +238,14 @@
|
||||
"schedule.settings.unnamed": "Unnamed Schedule",
|
||||
"schedule.settings.delete": "Delete",
|
||||
"schedule.settings.picker_title": "Select ClassIsland schedule file",
|
||||
"schedule.settings.picker_file_type": "ClassIsland CSES schedule",
|
||||
"schedule.settings.picker_file_type.all": "ClassIsland Schedule Files",
|
||||
"schedule.settings.picker_file_type.json": "ClassIsland Profile (JSON)",
|
||||
"schedule.settings.picker_file_type.cses": "CSES Schedule (YAML)",
|
||||
"schedule.settings.semester.title": "Semester Settings",
|
||||
"schedule.settings.semester.start_date": "Semester Start Date",
|
||||
"schedule.settings.semester.week_cycle": "Week Cycle",
|
||||
"schedule.settings.semester.week_cycle_desc": "Set the week rotation cycle for multi-week schedules (e.g., 2 for odd/even weeks).",
|
||||
"schedule.settings.semester.week_cycle_format": "{0}-week rotation",
|
||||
"worldclock.settings.title": "World Clock Settings",
|
||||
"worldclock.settings.desc": "Choose a time zone for each of the four clocks.",
|
||||
"worldclock.settings.clock_1": "Clock 1",
|
||||
@@ -236,6 +276,7 @@
|
||||
"settings.region.language_label": "Language",
|
||||
"settings.region.language_zh": "Chinese",
|
||||
"settings.region.language_en": "English",
|
||||
"settings.region.language_ja": "Japanese",
|
||||
"settings.region.timezone_header": "Time Zone",
|
||||
"settings.region.timezone_desc": "Select a time zone. Clock and calendar widgets will follow this zone.",
|
||||
"settings.region.applied_format": "Language switched to: {0}",
|
||||
@@ -291,8 +332,17 @@
|
||||
"settings.status_bar.clock_format.hm": "Hour:Minute",
|
||||
"settings.status_bar.clock_format.hms": "Hour:Minute:Second",
|
||||
"settings.components.title": "Components",
|
||||
"settings.components.description": "Adjust desktop grid density and widget placement.",
|
||||
"settings.components.grid_header": "Grid Layout",
|
||||
"settings.components.description": "Adjust component layout and corner design.",
|
||||
"settings.components.grid_header": "Grid Settings",
|
||||
"settings.components.header": "Grid Settings",
|
||||
"settings.components.short_side_label": "Short Side Cells",
|
||||
"settings.components.edge_inset_label": "Screen Inset",
|
||||
"settings.components.spacing_label": "Component Spacing",
|
||||
"settings.components.spacing_compact": "Compact",
|
||||
"settings.components.spacing_relaxed": "Relaxed",
|
||||
"settings.components.corner_radius.header": "Corner Design",
|
||||
"settings.components.corner_radius.label": "Component Corner Radius",
|
||||
"settings.components.corner_radius.description": "Adjust the shared corner radius from a square edge to a capsule-like shape, and expand the internal safe area with it.",
|
||||
"settings.update.title": "Update",
|
||||
"settings.update.current_version_label": "Current Version",
|
||||
"settings.update.latest_version_label": "Latest Release",
|
||||
@@ -368,6 +418,11 @@
|
||||
"settings.update.channel_preview_desc": "Preview builds may contain newer features but can be less stable.",
|
||||
"settings.update.download_threads_label": "Download Threads",
|
||||
"settings.update.download_threads_desc": "Set the number of parallel download threads for application update packages.",
|
||||
"settings.update.force_check_label": "Force Check Update",
|
||||
"settings.update.force_check_desc": "Force check for updates from GitHub, ignoring version comparison.",
|
||||
"settings.update.status_force_checking": "Force checking GitHub releases...",
|
||||
"settings.update.status_force_no_asset": "Release found but no compatible installer available.",
|
||||
"settings.update.status_force_available_format": "Release {0} is available. Click Download & Install.",
|
||||
"settings.update.install_now_button": "Install Now",
|
||||
"settings.update.status_downloaded_confirm": "Update downloaded. Review it and choose when to install.",
|
||||
"settings.update.status_downloaded_exit": "Update downloaded. It will be installed when you exit the app.",
|
||||
@@ -398,6 +453,7 @@
|
||||
"common.monet": "Monet",
|
||||
"desktop.page_index_format": "Desktop {0}",
|
||||
"launcher.title": "App Launcher",
|
||||
"launcher.folder": "Folder",
|
||||
"launcher.subtitle": "Apps and folders from Windows Start Menu",
|
||||
"launcher.subtitle_linux": "Installed apps discovered from Linux desktop entries",
|
||||
"launcher.empty": "No Start Menu entries found.",
|
||||
@@ -474,10 +530,10 @@
|
||||
"settings.plugins.source_manifest": "Loose manifest",
|
||||
"settings.plugins.subtitle_format": "{0} | {1} | {2}",
|
||||
"settings.plugins.detail_format": "Settings pages: {0} | Widgets: {1}",
|
||||
"settings.nav.plugin_market": "Plugin Market",
|
||||
"settings.plugin_market.title": "Plugin Market",
|
||||
"settings.plugin_market.subtitle": "Browse plugins from the official LanAirApp source and stage installs.",
|
||||
"settings.plugin_market.unavailable": "Plugin runtime is not available, so the official market cannot be opened right now.",
|
||||
"settings.nav.plugin_catalog": "Plugin Catalog",
|
||||
"settings.plugin_catalog.title": "Plugin Catalog",
|
||||
"settings.plugin_catalog.subtitle": "Browse plugins from the official LanAirApp source and stage installs.",
|
||||
"settings.plugin_catalog.unavailable": "Plugin runtime is not available, so the official catalog cannot be opened right now.",
|
||||
"settings.update.status_idle": "No update check has been performed yet.",
|
||||
"settings.update.status_preferences_saved": "Update preferences saved.",
|
||||
"settings.update.status_check_failed": "Failed to check for updates.",
|
||||
@@ -554,6 +610,7 @@
|
||||
"component_category.info": "Info",
|
||||
"component_category.calculator": "Calculator",
|
||||
"component_category.study": "Study",
|
||||
"component_category.file": "File",
|
||||
"component.date": "Calendar",
|
||||
"component.month_calendar": "Month Calendar",
|
||||
"component.lunar_calendar": "Lunar Calendar",
|
||||
@@ -582,6 +639,19 @@
|
||||
"component.blackboard_landscape": "Blackboard (Landscape)",
|
||||
"component.browser": "Browser",
|
||||
"component.office_recent_documents": "Recent Documents",
|
||||
"whiteboard.settings.desc": "Each blackboard keeps its own note history and saves it independently.",
|
||||
"whiteboard.settings.retention.title": "Note retention",
|
||||
"whiteboard.settings.retention.desc": "Choose how long this blackboard should keep saved notes before expired data is removed automatically.",
|
||||
"whiteboard.settings.retention.option": "{0} days",
|
||||
"whiteboard.settings.instance_scope": "This retention setting is stored per blackboard component instance.",
|
||||
"office_recent_documents.settings.desc": "Choose which Windows and Office sources this widget should scan for recent documents.",
|
||||
"office_recent_documents.settings.sources_title": "Recent document sources",
|
||||
"office_recent_documents.settings.sources_desc": "You can combine multiple sources. Registry selection also keeps the Office interop MRU fallback available.",
|
||||
"office_recent_documents.settings.source.registry": "Office registry MRU",
|
||||
"office_recent_documents.settings.source.recent_folders": "Windows Recent folders",
|
||||
"office_recent_documents.settings.source.jump_lists": "Windows Jump Lists",
|
||||
"office_recent_documents.settings.hint": "If you disable all sources, this widget will stay empty until at least one source is enabled again.",
|
||||
"component.removable_storage": "Removable Storage",
|
||||
"component.holiday_calendar": "Holiday Calendar",
|
||||
"component.study_environment": "Environment",
|
||||
"component.study_session_control": "Study Session Control",
|
||||
@@ -783,6 +853,20 @@
|
||||
"study.environment.settings.show_display_db": "Show display dB",
|
||||
"study.environment.settings.show_dbfs": "Show dBFS",
|
||||
"study.environment.settings.hint": "At least one display mode must stay enabled.",
|
||||
"removable_storage.settings.desc": "Show a connected USB drive with quick open and eject actions.",
|
||||
"removable_storage.settings.behavior_title": "Behavior",
|
||||
"removable_storage.settings.behavior_desc": "The widget automatically watches for removable drives and switches to the newest inserted USB drive.",
|
||||
"removable_storage.action.open": "Open",
|
||||
"removable_storage.action.eject": "Eject",
|
||||
"removable_storage.widget.default_name": "Removable Drive",
|
||||
"removable_storage.widget.empty_title": "No device inserted",
|
||||
"removable_storage.widget.empty_subtitle": "Insert a USB drive to show it here.",
|
||||
"removable_storage.widget.empty_hint": "Buttons stay disabled until a removable device is inserted.",
|
||||
"removable_storage.widget.ready": "Ready to open or eject.",
|
||||
"removable_storage.widget.ejecting": "Ejecting drive...",
|
||||
"removable_storage.widget.eject_failed": "Could not eject this drive. Close any files on it and try again.",
|
||||
"removable_storage.widget.open_failed": "Failed to open this drive.",
|
||||
"removable_storage.widget.refresh_failed": "Drive list refresh failed.",
|
||||
"study.session_control.action.start": "Start Study Session",
|
||||
"study.session_control.action.stop": "Stop Study Session",
|
||||
"study.session_control.idle_hint": "Tap the right button to start",
|
||||
@@ -880,6 +964,10 @@
|
||||
"study.interrupt_density.unavailable": "--",
|
||||
"desktop.add_page": "Add page",
|
||||
"desktop.delete_page": "Delete page",
|
||||
"desktop.delete_page_confirm.title": "Confirm Delete Page",
|
||||
"desktop.delete_page_confirm.message": "Are you sure you want to delete the current page?\n\nThis will remove all components on this page and cannot be undone.",
|
||||
"desktop.delete_page_confirm.primary": "Delete",
|
||||
"desktop.delete_page_confirm.close": "Cancel",
|
||||
"placement.fill": "Fill",
|
||||
"placement.fit": "Fit",
|
||||
"placement.stretch": "Stretch",
|
||||
@@ -887,5 +975,7 @@
|
||||
"placement.tile": "Tile",
|
||||
"single_instance.notice.title": "App already running",
|
||||
"single_instance.notice.description": "The app is already running. There is no need to click multiple times to open it.",
|
||||
"single_instance.notice.button": "OK"
|
||||
"single_instance.notice.button": "OK",
|
||||
"market.status.install_success_restart_format": "✓ Plugin '{0}' installed successfully! Please restart the application to activate it.",
|
||||
"market.dialog.restart_message_format": "Plugin '{0}' has been installed successfully.\n\nTo use this plugin, you need to restart the application now.\n\nWould you like to restart?"
|
||||
}
|
||||
|
||||
978
LanMountainDesktop/Localization/ja-JP.json
Normal file
978
LanMountainDesktop/Localization/ja-JP.json
Normal file
@@ -0,0 +1,978 @@
|
||||
{
|
||||
"app.title": "LanMountainDesktop",
|
||||
"tray.tooltip": "LanMountainDesktop",
|
||||
"tray.menu.show_desktop": "デスクトップを開く",
|
||||
"tray.menu.settings": "設定",
|
||||
"tray.menu.component_library": "ウィジェットライブラリ",
|
||||
"tray.menu.restart": "アプリを再起動",
|
||||
"tray.menu.exit": "アプリを終了",
|
||||
"button.back_to_windows": "Windowsに戻る",
|
||||
"button.back_to_platform": "{0}に戻る",
|
||||
"tooltip.back_to_windows": "Windowsに戻る",
|
||||
"tooltip.back_to_platform": "{0}に戻る",
|
||||
"platform.windows": "Windows",
|
||||
"platform.linux": "Linux",
|
||||
"platform.macos": "macOS",
|
||||
"tooltip.open_settings": "設定",
|
||||
"settings.title": "設定",
|
||||
"settings.shell.title": "設定",
|
||||
"settings.shell.subtitle": "LanMountainDesktop 独立設定モジュール",
|
||||
"settings.shell.sidebar_hint": "カテゴリを選択して、アプリの動作、デスクトップレイアウト、外観を調整します。",
|
||||
"settings.shell.footer_hint": "トレイから開く設定は、この独立設定モジュールで管理されます。",
|
||||
"settings.back_to_desktop": "デスクトップに戻る",
|
||||
"settings.nav_header": "設定",
|
||||
"settings.nav.group_desktop": "デスクトップ",
|
||||
"settings.nav.group_system": "システム",
|
||||
"settings.nav.group_extensions": "拡張機能",
|
||||
"settings.nav.wallpaper": "壁紙",
|
||||
"settings.nav.grid": "コンポーネント",
|
||||
"settings.nav.color": "カラー",
|
||||
"settings.nav.status_bar": "ステータスバー",
|
||||
"settings.nav.weather": "天気",
|
||||
"settings.nav.region": "地域",
|
||||
"settings.nav.update": "アップデート",
|
||||
"settings.nav.privacy": "プライバシー",
|
||||
"settings.nav.launcher": "アプリランチャー",
|
||||
"settings.nav.plugins": "プラグイン",
|
||||
"settings.nav.about": "について",
|
||||
"settings.wallpaper.title": "壁紙",
|
||||
"settings.wallpaper.description": "画像または動画を選択して、アプリウィンドウの壁紙としてすぐに適用します。",
|
||||
"settings.wallpaper.current_label": "現在の壁紙",
|
||||
"settings.wallpaper.type_label": "壁紙タイプ",
|
||||
"settings.wallpaper.type.image": "画像",
|
||||
"settings.wallpaper.type.solid_color": "単色",
|
||||
"settings.wallpaper.type.system": "システム壁紙",
|
||||
"settings.wallpaper.system.label": "システム壁紙",
|
||||
"settings.wallpaper.system.unavailable": "システム壁紙を読み込めません",
|
||||
"settings.wallpaper.refresh_interval": "更新間隔",
|
||||
"settings.wallpaper.refresh_now": "今すぐ更新",
|
||||
"settings.wallpaper.refresh.30s": "30秒",
|
||||
"settings.wallpaper.refresh.1m": "1分",
|
||||
"settings.wallpaper.refresh.5m": "5分",
|
||||
"settings.wallpaper.refresh.10m": "10分",
|
||||
"settings.wallpaper.refresh.15m": "15分",
|
||||
"settings.wallpaper.refresh.30m": "30分",
|
||||
"settings.wallpaper.refresh.1h": "1時間",
|
||||
"settings.wallpaper.refresh.2h": "2時間",
|
||||
"settings.wallpaper.refresh.4h": "4時間",
|
||||
"settings.wallpaper.refresh.8h": "8時間",
|
||||
"settings.wallpaper.refresh.12h": "12時間",
|
||||
"settings.wallpaper.refresh.24h": "24時間",
|
||||
"settings.wallpaper.color_label": "壁紙の色",
|
||||
"settings.wallpaper.placement_label": "配置",
|
||||
"settings.wallpaper.placement_desc": "画像がデスクトップにどのように表示されるかを調整します。",
|
||||
"settings.wallpaper.pick_button": "ファイルを参照",
|
||||
"settings.wallpaper.clear_button": "単色にリセット",
|
||||
"settings.wallpaper.no_selection": "壁紙が選択されていません。",
|
||||
"settings.wallpaper.storage_unavailable": "ストレージプロバイダが利用できません。",
|
||||
"settings.wallpaper.import_failed": "壁紙ファイルのインポートに失敗しました。",
|
||||
"settings.wallpaper.image_applied": "画像の壁紙が適用されました。",
|
||||
"settings.wallpaper.video_applied": "動画の壁紙が適用されました。",
|
||||
"settings.wallpaper.unsupported_file": "選択されたファイルタイプはサポートされていません。",
|
||||
"settings.wallpaper.apply_failed_format": "壁紙の適用に失敗しました: {0}",
|
||||
"settings.wallpaper.mode_format": "壁紙モード: {0}。",
|
||||
"settings.wallpaper.video_mode": "動画の壁紙は自動フィルモードを使用します。",
|
||||
"settings.wallpaper.cleared": "背景が単色にリセットされました。",
|
||||
"settings.wallpaper.default_status": "現在の背景は単色を使用しています。",
|
||||
"settings.wallpaper.saved_not_found": "保存された壁紙ファイルが見つかりません。単色の背景を使用しています。",
|
||||
"settings.wallpaper.restored": "保存された設定から壁紙が復元されました。",
|
||||
"settings.wallpaper.video_restored": "保存された設定から動画の壁紙が復元されました。",
|
||||
"settings.wallpaper.restore_failed": "保存された壁紙の復元に失敗しました。単色の背景を使用しています。",
|
||||
"settings.wallpaper.video_not_found": "動画の壁紙ファイルが見つかりません。",
|
||||
"settings.wallpaper.video_player_unavailable": "動画プレーヤーが利用できません。",
|
||||
"settings.wallpaper.video_play_failed_format": "動画の壁紙の再生に失敗しました: {0}",
|
||||
"settings.grid.title": "グリッドレイアウト",
|
||||
"settings.grid.description": "すべてのコンポーネントは少なくとも1つのセルを占有する必要があります(最小1x1)。",
|
||||
"settings.grid.short_side_label": "短辺のセル数",
|
||||
"settings.grid.spacing_label": "グリッドの間隔",
|
||||
"settings.grid.spacing_relaxed": "ゆとりあり(iOS)",
|
||||
"settings.grid.spacing_compact": "コンパクト(Android)",
|
||||
"settings.grid.edge_inset_label": "画面の余白",
|
||||
"settings.grid.edge_inset_px_format": "≈ {0:F1}px",
|
||||
"settings.grid.apply_button": "適用",
|
||||
"settings.grid.info_format": "グリッド: {0}列 x {1}行 | セル {2:F1}px (1:1)",
|
||||
"settings.color.title": "カラー",
|
||||
"settings.color.description": "昼夜モードを切り替え、アプリのアクセントカラーを選択します。",
|
||||
"settings.color.day_night_label": "昼夜モード",
|
||||
"settings.color.day_night_on": "夜",
|
||||
"settings.color.day_night_off": "昼",
|
||||
"settings.color.recommended_label": "おすすめの色",
|
||||
"settings.color.system_monet_label": "システムMonetカラー",
|
||||
"settings.color.refresh_button": "更新",
|
||||
"settings.color.mode_night": "夜モードが有効",
|
||||
"settings.color.mode_day": "昼モードが有効",
|
||||
"settings.color.mode_status_format": "テーマモード: {0}。",
|
||||
"settings.color.monet_refreshed": "Monetカラーが更新されました。",
|
||||
"settings.color.theme_ready_format": "テーマカラーの準備完了: {0}。",
|
||||
"settings.color.theme_applied_format": "{0}カラーが適用されました: {1}。",
|
||||
"settings.color.theme_updated_wallpaper": "壁紙が更新されました。Monetカラーが更新されました。",
|
||||
"settings.color.theme_updated_video": "動画の壁紙が更新されました。テーマカラーが更新されました。",
|
||||
"settings.color.theme_cleared_wallpaper": "壁紙がクリアされました。Monetカラーが更新されました。",
|
||||
"settings.status_bar.title": "ステータスバー",
|
||||
"settings.status_bar.description": "上部のステータスバーに表示するコンポーネントを選択します。",
|
||||
"settings.status_bar.clock_header": "時計コンポーネント",
|
||||
"settings.status_bar.clock_description": "上部のステータスバーに時計を表示します。",
|
||||
"settings.status_bar.clock_transparent_background_label": "透明な背景",
|
||||
"settings.status_bar.clock_transparent_background_desc": "カプセルの背景を削除し、時計のテキストのみを保持します。",
|
||||
"settings.status_bar.spacing_header": "コンポーネントの間隔",
|
||||
"settings.status_bar.spacing_desc": "ステータスバーコンポーネント間の間隔を調整します。",
|
||||
"settings.status_bar.spacing_mode_compact": "コンパクト",
|
||||
"settings.status_bar.spacing_mode_relaxed": "ゆとりあり",
|
||||
"settings.status_bar.spacing_mode_custom": "カスタム",
|
||||
"settings.status_bar.spacing_custom_label": "カスタム間隔(%)",
|
||||
"settings.status_bar.spacing_custom_px_format": "≈ {0:F1}px",
|
||||
"settings.privacy.title": "プライバシー",
|
||||
"settings.privacy.description": "アプリの改善に役立つオプションの匿名アップロードを管理します。",
|
||||
"settings.privacy.crash_upload_title": "匿名クラッシュデータのアップロード",
|
||||
"settings.privacy.crash_upload_description": "アプリケーションの安定性向上にご協力ください。",
|
||||
"settings.privacy.usage_upload_title": "匿名使用データのアップロード",
|
||||
"settings.privacy.usage_upload_description": "アプリケーション機能の改善にご協力ください。",
|
||||
"settings.privacy.device_id_title": "デバイスID",
|
||||
"settings.privacy.device_id_description": "このデバイスの一意識別子。更新をクリックして再生成します。",
|
||||
"settings.privacy.refresh_device_id": "更新",
|
||||
"settings.privacy.policy_hint_prefix": "詳細については、",
|
||||
"settings.privacy.view_policy": "プライバシーポリシーをご覧ください",
|
||||
"settings.weather.title": "天気",
|
||||
"settings.weather.description": "天気の場所、Xiaomi天気プレビュー、起動時の位置情報取得動作を設定します。",
|
||||
"settings.weather.location_source_header": "位置情報ソース",
|
||||
"settings.weather.location_source_desc": "天気ウィジェットが場所を解決する方法を選択します。",
|
||||
"settings.weather.mode_city_search": "都市検索",
|
||||
"settings.weather.mode_coordinates": "座標",
|
||||
"settings.weather.auto_refresh": "起動時に位置情報を自動更新",
|
||||
"settings.weather.city_search_header": "都市検索",
|
||||
"settings.weather.city_search_desc": "都市を検索し、天気の場所を適用します。",
|
||||
"settings.weather.search_placeholder": "例: 東京",
|
||||
"settings.weather.search_button": "検索",
|
||||
"settings.weather.apply_city_button": "都市を適用",
|
||||
"settings.weather.search_hint": "都市名で検索し、場所を適用します。",
|
||||
"settings.weather.search_required": "都市のキーワードを入力してください。",
|
||||
"settings.weather.search_no_results": "場所が見つかりませんでした。",
|
||||
"settings.weather.search_failed_format": "検索に失敗しました: {0}",
|
||||
"settings.weather.search_result_count_format": "{0}件の場所が見つかりました。",
|
||||
"settings.weather.search_select_required": "検索結果から場所を1つ選択してください。",
|
||||
"settings.weather.search_applied_format": "場所が適用されました: {0}",
|
||||
"settings.weather.coordinates_header": "座標",
|
||||
"settings.weather.coordinates_desc": "緯度/経度とオプションのキー/名前を設定します。",
|
||||
"settings.weather.latitude_label": "緯度",
|
||||
"settings.weather.longitude_label": "経度",
|
||||
"settings.weather.location_key_placeholder": "場所キー(オプション)",
|
||||
"settings.weather.location_name_placeholder": "表示名(オプション)",
|
||||
"settings.weather.apply_coordinates_button": "座標を適用",
|
||||
"settings.weather.coordinates_saved_format": "座標が保存されました: {0:F4}, {1:F4}",
|
||||
"settings.weather.coordinates_default_name_format": "座標 {0:F4}, {1:F4}",
|
||||
"settings.weather.location_services_header": "位置情報サービス",
|
||||
"settings.weather.location_services_desc": "現在のWindowsの場所を使用し、起動時に自動的に更新するかどうかを決定します。",
|
||||
"settings.weather.use_current_location": "現在地を使用",
|
||||
"settings.weather.location_unsupported": "現在のプラットフォームは現在地の取得をサポートしていません。",
|
||||
"settings.weather.location_ready": "現在のWindowsの場所を使用できます。",
|
||||
"settings.weather.location_refreshing": "現在地を取得中...",
|
||||
"settings.weather.location_refresh_success_format": "現在地が適用されました: {0}",
|
||||
"settings.weather.location_refresh_failed_format": "現在地の取得に失敗しました: {0}",
|
||||
"settings.weather.preview_header": "接続テスト",
|
||||
"settings.weather.preview_desc": "テストリクエストを送信して現在の設定を確認します。",
|
||||
"settings.weather.preview_button": "テスト取得",
|
||||
"settings.weather.preview_section": "天気プレビュー",
|
||||
"settings.weather.settings_section": "設定",
|
||||
"settings.weather.preview_panel_header": "天気プレビュー",
|
||||
"settings.weather.preview_panel_desc": "現在の天気サービスの状態を更新して確認します。",
|
||||
"settings.weather.refresh_button": "更新",
|
||||
"settings.weather.preview_updated_format": "{0}に更新",
|
||||
"settings.weather.preview_hint": "テスト取得を使用して天気の設定を確認します。",
|
||||
"settings.weather.preview_missing_location": "テストする前に天気の場所を適用してください。",
|
||||
"settings.weather.preview_success_format": "テスト成功: {0} · {1} · {2}",
|
||||
"settings.weather.preview_failed_format": "テスト取得に失敗しました: {0}",
|
||||
"settings.weather.preview_unknown": "不明",
|
||||
"settings.weather.alert_filter_header": "除外するアラート",
|
||||
"settings.weather.alert_filter_desc": "これらの単語を含むアラートは表示されません。1行に1つのルール。",
|
||||
"settings.weather.alert_filter_placeholder": "1行に1つのキーワード",
|
||||
"settings.weather.icon_style_header": "天気アイコンスタイル",
|
||||
"settings.weather.icon_style_desc": "天気シンボルのFluentアイコンスタイルを選択します。",
|
||||
"settings.weather.icon_style_fluent_regular": "Fluent Regular",
|
||||
"settings.weather.icon_style_fluent_filled": "Fluent Filled",
|
||||
"settings.weather.no_tls_header": "TLSなしの天気リクエスト",
|
||||
"settings.weather.no_tls_desc": "推奨されません。互換性のないネットワーク環境でのみ有効にしてください。",
|
||||
"settings.weather.status_city_empty": "都市の場所が設定されていません。",
|
||||
"settings.weather.status_city_format": "モード: {0} | {1} | キー: {2}",
|
||||
"settings.weather.status_coordinates_format": "モード: {0} | 緯度 {1:F4}, 経度 {2:F4} | キー: {3}",
|
||||
"settings.weather.city_selection_label": "都市選択",
|
||||
"settings.weather.coordinates_selection_label": "座標の場所",
|
||||
"settings.weather.location_city_summary_desc": "天気の照会に使用される現在の都市を選択します。",
|
||||
"settings.weather.location_coordinates_summary_desc": "天気の照会に使用される緯度/経度とオプションの場所名を設定します。",
|
||||
"settings.weather.location_not_selected": "場所が選択されていません",
|
||||
"settings.weather.alert_list_label": "除外リスト",
|
||||
"settings.weather.alert_list_desc": "1行に1つの除外ルール。",
|
||||
"settings.weather.no_tls_toggle": "非TLSリクエストのフォールバックを許可",
|
||||
"settings.weather.footer_hint": "デスクトップ天気ウィジェットは、ここで設定された場所とアラート除外設定を再利用します。",
|
||||
"settings.weather.location_header": "天気の場所",
|
||||
"settings.weather.location_desc": "天気ウィジェットで使用する場所を設定します。",
|
||||
"settings.weather.location_placeholder": "例: 東京",
|
||||
"settings.weather.location_apply": "保存",
|
||||
"settings.weather.location_empty": "天気の場所が設定されていません。",
|
||||
"settings.weather.location_required": "天気の場所は空にできません。",
|
||||
"settings.weather.location_current_format": "現在の天気の場所: {0}",
|
||||
"settings.weather.location_saved_format": "天気の場所が保存されました: {0}",
|
||||
"weather.widget.location_not_configured": "天気の場所が設定されていません",
|
||||
"weather.widget.configure_hint": "設定 > 天気を開いて設定",
|
||||
"weather.widget.loading": "読み込み中...",
|
||||
"weather.widget.fetch_failed": "天気の取得に失敗しました",
|
||||
"weather.widget.retrying": "自動的に再試行中",
|
||||
"weather.widget.location_unknown": "不明な場所",
|
||||
"weather.widget.condition_clear": "晴れ",
|
||||
"weather.widget.condition_cloudy": "曇り",
|
||||
"weather.widget.condition_rain": "雨",
|
||||
"weather.widget.condition_storm": "雷雨",
|
||||
"weather.widget.condition_snow": "雪",
|
||||
"weather.widget.condition_fog": "霧",
|
||||
"weather.widget.condition_unknown": "不明",
|
||||
"weather.widget.range_unknown": "-- / --",
|
||||
"weather.widget.range_format": "{0} / {1}",
|
||||
"schedule.widget.no_source": "ClassIslandのスケジュールデータが見つかりません",
|
||||
"schedule.widget.no_class_today": "今日の授業はありません",
|
||||
"schedule.widget.layout_missing": "スケジュールの時間レイアウトがありません",
|
||||
"schedule.widget.subject_fallback": "無題の授業",
|
||||
"schedule.widget.detail_fallback": "詳細なし",
|
||||
"schedule.settings.title": "スケジュールのインポート",
|
||||
"schedule.settings.desc": "ClassIsland CSESスケジュールをインポートし、有効にするものを選択します。",
|
||||
"schedule.settings.add": "スケジュールを追加",
|
||||
"schedule.settings.empty": "インポートされたスケジュールはありません",
|
||||
"schedule.settings.unnamed": "無題のスケジュール",
|
||||
"schedule.settings.delete": "削除",
|
||||
"schedule.settings.picker_title": "ClassIslandスケジュールファイルを選択",
|
||||
"schedule.settings.picker_file_type.all": "ClassIslandスケジュールファイル",
|
||||
"schedule.settings.picker_file_type.json": "ClassIslandプロファイル(JSON)",
|
||||
"schedule.settings.picker_file_type.cses": "CSESスケジュール(YAML)",
|
||||
"schedule.settings.semester.title": "学期設定",
|
||||
"schedule.settings.semester.start_date": "学期開始日",
|
||||
"schedule.settings.semester.week_cycle": "週サイクル",
|
||||
"schedule.settings.semester.week_cycle_desc": "複数週スケジュールの週ローテーションサイクルを設定します(例: 奇数週/偶数週の場合は2)。",
|
||||
"schedule.settings.semester.week_cycle_format": "{0}週ローテーション",
|
||||
"worldclock.settings.title": "世界時計の設定",
|
||||
"worldclock.settings.desc": "4つの時計それぞれのタイムゾーンを選択します。",
|
||||
"worldclock.settings.clock_1": "時計 1",
|
||||
"worldclock.settings.clock_2": "時計 2",
|
||||
"worldclock.settings.clock_3": "時計 3",
|
||||
"worldclock.settings.clock_4": "時計 4",
|
||||
"worldclock.settings.second_mode_label": "秒針",
|
||||
"worldclock.widget.today": "今日",
|
||||
"worldclock.widget.yesterday": "昨日",
|
||||
"worldclock.widget.tomorrow": "明日",
|
||||
"worldclock.widget.offset_same": "0時間",
|
||||
"worldclock.widget.offset_ahead_hours": "{0}時間進む",
|
||||
"worldclock.widget.offset_behind_hours": "{0}時間遅れる",
|
||||
"worldclock.widget.offset_ahead_hm": "{0}時間{1}分進む",
|
||||
"worldclock.widget.offset_behind_hm": "{0}時間{1}分遅れる",
|
||||
"weather.widget.aqi_unknown": "AQI --",
|
||||
"weather.widget.aqi_format": "AQI {0}",
|
||||
"weather.widget.updated_format": "{0:HH:mm}に更新",
|
||||
"weather.hourly.now": "現在",
|
||||
"weather.hourly.sunset": "日没",
|
||||
"weather.multiday.today": "今日",
|
||||
"weather.multiday.tomorrow": "明日",
|
||||
"weather.multiday.aqi_format": "空気質 {0}",
|
||||
"weather.multiday.aqi_unknown": "空気質 --",
|
||||
"settings.region.title": "地域",
|
||||
"settings.region.description": "言語を選択し、設定と主要なUIにすぐに適用します。",
|
||||
"settings.region.language_header": "言語",
|
||||
"settings.region.language_label": "言語",
|
||||
"settings.region.language_zh": "中国語",
|
||||
"settings.region.language_en": "英語",
|
||||
"settings.region.language_ja": "日本語",
|
||||
"settings.region.timezone_header": "タイムゾーン",
|
||||
"settings.region.timezone_desc": "タイムゾーンを選択します。時計とカレンダーウィジェットはこのゾーンに従います。",
|
||||
"settings.region.applied_format": "言語が切り替わりました: {0}",
|
||||
"settings.region.follow_system": "システムの既定に従う",
|
||||
"settings.general.title": "一般",
|
||||
"settings.general.description": "言語、タイムゾーン、ランタイムの動作を調整します。",
|
||||
"settings.general.basic_header": "基本設定",
|
||||
"settings.general.runtime_header": "ランタイム",
|
||||
"settings.general.preview_header": "日時プレビュー",
|
||||
"settings.general.preview_time_label": "時刻",
|
||||
"settings.general.preview_date_label": "日付",
|
||||
"settings.general.render_mode_restart_message": "レンダリングモードの変更にはアプリの再起動が必要です。",
|
||||
"settings.appearance.title": "外観",
|
||||
"settings.appearance.description": "テーマソース、システムマテリアル、ウィンドウクロームを調整します。",
|
||||
"settings.appearance.theme_header": "テーマ",
|
||||
"settings.color.enable_night_mode_toggle": "夜モードを有効にする",
|
||||
"settings.color.use_system_chrome_toggle": "システムのウィンドウクロームを使用",
|
||||
"settings.color.theme_color_label": "テーマのアクセントカラー",
|
||||
"settings.appearance.theme_color_mode_label": "テーマカラーソース",
|
||||
"settings.appearance.theme_color_mode.neutral": "デフォルトニュートラル",
|
||||
"settings.appearance.theme_color_mode.user": "ユーザーテーマカラーMonet",
|
||||
"settings.appearance.theme_color_mode.wallpaper": "壁紙Monet",
|
||||
"settings.appearance.theme_color_mode_desc.neutral": "ライトモードとダークモードにデフォルトの白と黒のニュートラルサーフェスを使用します。",
|
||||
"settings.appearance.theme_color_mode_desc.user": "選択したテーマカラーをシェル全体のMonetシードとして使用します。",
|
||||
"settings.appearance.theme_color_mode_desc.wallpaper": "壁紙の色を使用します。アプリの壁紙が優先され、次にシステムの壁紙が使用されます。",
|
||||
"settings.appearance.theme_color_preview.app": "現在、アプリの壁紙から抽出された色をプレビューしています。",
|
||||
"settings.appearance.theme_color_preview.system": "現在、システムの壁紙から抽出された色をプレビューしています。",
|
||||
"settings.appearance.theme_color_preview.fallback": "使用可能な壁紙が見つかりませんでした。アプリはフォールバックのアクセントを使用しています。",
|
||||
"component.color_scheme.follow_system": "システムのカラースキームに従う",
|
||||
"component.color_scheme.native": "コンポーネントのカスタムカラースキームを使用",
|
||||
"settings.appearance.system_material.none": "なし",
|
||||
"settings.appearance.system_material.mica": "Mica",
|
||||
"settings.appearance.system_material.acrylic": "Acrylic",
|
||||
"settings.appearance.system_material_desc.switchable": "選択したマテリアルをウィンドウ、Dock、ステータスバー、コンポーネントホストに適用します。",
|
||||
"settings.appearance.system_material_desc.fixed": "現在のシステムは、ここにリストされているマテリアルモードのみを公開しています。",
|
||||
"settings.appearance.restart_message": "テーマソースとシステムマテリアルの変更にはアプリの再起動が必要です。",
|
||||
"settings.appearance.preview.primary": "プライマリ",
|
||||
"settings.appearance.preview.secondary": "セカンダリ",
|
||||
"settings.appearance.preview.tertiary": "ターシャリ",
|
||||
"settings.appearance.preview.neutral": "ニュートラル",
|
||||
"settings.appearance.preview.seed": "シード",
|
||||
"settings.appearance.preview.neutral_light": "白",
|
||||
"settings.appearance.preview.neutral_dark": "黒",
|
||||
"settings.appearance.preview.apply_seed": "適用",
|
||||
"settings.appearance.preview.wallpaper_candidates": "壁紙シード候補",
|
||||
"settings.appearance.preview.wallpaper_current": "現在",
|
||||
"settings.wallpaper.placement.fill": "フィル",
|
||||
"settings.wallpaper.placement.fit": "フィット",
|
||||
"settings.wallpaper.placement.stretch": "ストレッチ",
|
||||
"settings.wallpaper.placement.center": "中央",
|
||||
"settings.wallpaper.placement.tile": "タイル",
|
||||
"settings.status_bar.clock_format_label": "時計の形式",
|
||||
"settings.status_bar.clock_format.hm": "時:分",
|
||||
"settings.status_bar.clock_format.hms": "時:分:秒",
|
||||
"settings.components.title": "コンポーネント",
|
||||
"settings.components.description": "コンポーネントのレイアウトとコーナーデザインを調整します。",
|
||||
"settings.components.grid_header": "グリッド設定",
|
||||
"settings.components.header": "グリッド設定",
|
||||
"settings.components.short_side_label": "短辺のセル数",
|
||||
"settings.components.edge_inset_label": "画面の余白",
|
||||
"settings.components.spacing_label": "コンポーネントの間隔",
|
||||
"settings.components.spacing_compact": "コンパクト",
|
||||
"settings.components.spacing_relaxed": "ゆとりあり",
|
||||
"settings.components.corner_radius.header": "コーナーデザイン",
|
||||
"settings.components.corner_radius.label": "コンポーネントのコーナー半径",
|
||||
"settings.components.corner_radius.description": "角張った端からカプセルのような形まで、共通のコーナー半径を調整し、内部のセーフエリアを拡張します。",
|
||||
"settings.update.title": "アップデート",
|
||||
"settings.update.current_version_label": "現在のバージョン",
|
||||
"settings.update.latest_version_label": "最新リリース",
|
||||
"settings.update.published_at_label": "公開日",
|
||||
"settings.update.options_header": "アップデートオプション",
|
||||
"settings.update.options_desc": "アップデートチェックとリリースチャンネルを設定します。",
|
||||
"settings.update.auto_check_toggle": "起動時に自動的にアップデートを確認",
|
||||
"settings.update.include_prerelease_toggle": "プレリリース版を含める",
|
||||
"settings.update.channel_label": "アップデートチャンネル",
|
||||
"settings.update.channel_stable": "安定版",
|
||||
"settings.update.channel_preview": "プレビュー",
|
||||
"settings.update.actions_header": "アップデートアクション",
|
||||
"settings.update.actions_desc": "リリースを確認し、インストーラーをダウンロードし、アップデートを開始します。",
|
||||
"settings.update.check_button": "アップデートを確認",
|
||||
"settings.update.download_install_button": "ダウンロードしてインストール",
|
||||
"settings.update.download_progress_idle": "ダウンロード進捗: -",
|
||||
"settings.update.download_progress_format": "ダウンロード進捗: {0:F0}%",
|
||||
"settings.update.status_ready": "アップデートを確認する準備ができました。",
|
||||
"settings.update.status_channel_changed": "アップデートチャンネルが変更されました。再度確認してください。",
|
||||
"settings.update.status_channel_changed_format": "アップデートチャンネルが{0}に切り替わりました。再度確認してください。",
|
||||
"settings.update.status_windows_only": "自動インストーラーアップデートは現在Windowsでのみ利用可能です。",
|
||||
"settings.update.status_checking": "GitHubリリースを確認中...",
|
||||
"settings.update.status_check_failed_format": "アップデートの確認に失敗しました: {0}",
|
||||
"settings.update.status_up_to_date": "最新バージョンを使用しています。",
|
||||
"settings.update.status_asset_missing": "新しいリリースが利用可能ですが、互換性のあるインストーラーが見つかりませんでした。",
|
||||
"settings.update.status_available_format": "新しいバージョン{0}が利用可能です。ダウンロードしてインストールをクリックしてください。",
|
||||
"settings.update.status_downloading": "インストーラーをダウンロード中...",
|
||||
"settings.update.status_download_failed_format": "ダウンロードに失敗しました: {0}",
|
||||
"settings.update.status_launching_installer": "ダウンロード完了。インストーラーを起動中...",
|
||||
"settings.update.status_installer_missing": "ダウンロード後にインストーラーファイルが見つかりませんでした。",
|
||||
"settings.update.status_installer_started": "インストーラーが開始されました。アプリはアップデートのために終了します。",
|
||||
"settings.update.status_elevation_cancelled": "管理者権限が付与されませんでした。アップデートはキャンセルされました。",
|
||||
"settings.update.status_launch_failed_format": "インストーラーの起動に失敗しました: {0}",
|
||||
"settings.about.title": "について",
|
||||
"settings.about.version_format": "バージョン: {0}",
|
||||
"settings.about.codename_format": "コードネーム: {0}",
|
||||
"settings.about.font_format": "フォント: {0}",
|
||||
"settings.about.startup_header": "Windowsのスタートアップ",
|
||||
"settings.about.startup_desc": "Windowsへのサインイン時にアプリを自動的に起動します。",
|
||||
"settings.about.startup_toggle": "Windowsサインイン時に起動",
|
||||
"settings.about.render_mode_header": "アプリのレンダリングモード",
|
||||
"settings.about.render_mode_desc": "レンダリングバックエンドを選択します。このオプションを変更した後、アプリを再起動します。サポートされていないモードはソフトウェアにフォールバックします。",
|
||||
"settings.about.render_mode.default": "デフォルト",
|
||||
"settings.about.render_mode.software": "ソフトウェア",
|
||||
"settings.about.render_mode.angle_egl": "angleEgl",
|
||||
"settings.about.render_mode.wgl": "WGL",
|
||||
"settings.about.render_mode.vulkan": "Vulkan",
|
||||
"settings.about.render_mode.unknown": "不明",
|
||||
"settings.about.render_mode.current_label": "現在の実際のバックエンド",
|
||||
"settings.about.render_mode.current_format": "現在のバックエンド: {0}",
|
||||
"settings.about.render_mode.impl_format": "ランタイム実装: {0}",
|
||||
"settings.about.render_mode.impl_unavailable": "ランタイム実装の詳細は利用できません。",
|
||||
"settings.about.description": "アプリケーションの詳細。",
|
||||
"settings.update.description": "リリースを確認し、アップデートチャンネルとダウンロードソースを選択し、アップデートのインストール方法を制御します。",
|
||||
"settings.update.status_card_title": "アップデートステータス",
|
||||
"settings.update.status_card_description": "アップデートを確認し、リリースの詳細を確認し、新しいバージョンが利用可能な場合はダウンロードまたはインストールを続行します。",
|
||||
"settings.update.preferences_header": "アップデート設定",
|
||||
"settings.update.preferences_description": "リリースチャンネル、インストーラーのダウンロードソース、インストール動作、ダウンロードの並列度を選択します。",
|
||||
"settings.update.last_checked_label": "最終確認日時",
|
||||
"settings.update.source_label": "ダウンロードソース",
|
||||
"settings.update.source_github": "GitHub",
|
||||
"settings.update.source_ghproxy": "gh-proxy",
|
||||
"settings.update.source_github_desc": "GitHubからリリースアセットを直接ダウンロードします。",
|
||||
"settings.update.source_ghproxy_desc": "GitHubリリースアセットをダウンロードする際にgh-proxyミラーを使用します。",
|
||||
"settings.update.mode_label": "アップデートモード",
|
||||
"settings.update.mode_manual": "手動アップデート",
|
||||
"settings.update.mode_download_then_confirm": "サイレントダウンロード",
|
||||
"settings.update.mode_silent_on_exit": "サイレントインストール",
|
||||
"settings.update.mode_manual_desc": "アップデートの確認のみ。ダウンロードとインストールのタイミングを決定します。",
|
||||
"settings.update.mode_download_then_confirm_desc": "バックグラウンドでアップデートをダウンロードし、インストール前に確認を求めます。",
|
||||
"settings.update.mode_silent_on_exit_desc": "バックグラウンドでアップデートをダウンロードし、アプリの終了時にインストールします。",
|
||||
"settings.update.channel_stable_desc": "安定ビルドは信頼性を重視し、ほとんどのユーザーにおすすめです。",
|
||||
"settings.update.channel_preview_desc": "プレビュービルドは新しい機能が含まれる可能性がありますが、安定性が低い場合があります。",
|
||||
"settings.update.download_threads_label": "ダウンロードスレッド",
|
||||
"settings.update.download_threads_desc": "アプリケーションのアップデートパッケージの並列ダウンロードスレッド数を設定します。",
|
||||
"settings.update.force_check_label": "強制アップデート確認",
|
||||
"settings.update.force_check_desc": "GitHubから強制的に最新バージョンを取得し、バージョン比較を無視します。",
|
||||
"settings.update.status_force_checking": "GitHubリリースを強制確認中...",
|
||||
"settings.update.status_force_no_asset": "リリースは見つかりましたが、互換性のあるインストーラーがありません。",
|
||||
"settings.update.status_force_available_format": "リリース {0} が利用可能です。「ダウンロードしてインストール」をクリックしてください。",
|
||||
"settings.update.install_now_button": "今すぐインストール",
|
||||
"settings.update.status_downloaded_confirm": "アップデートがダウンロードされました。確認してインストールのタイミングを選択してください。",
|
||||
"settings.update.status_downloaded_exit": "アップデートがダウンロードされました。アプリの終了時にインストールされます。",
|
||||
"settings.about.app_info_header": "アプリケーション情報",
|
||||
"settings.about.update_header": "アップデート",
|
||||
"settings.about.version_label": "バージョン",
|
||||
"settings.about.codename_label": "コードネーム",
|
||||
"settings.about.render_backend_label": "レンダーバックエンド",
|
||||
"settings.about.render_backend_format": "レンダーバックエンド: {0}",
|
||||
"settings.restart_dialog.title": "再起動が必要",
|
||||
"settings.restart_dialog.render_mode_message": "レンダリングモードを「{0}」から「{1}」に切り替えるには、アプリを再起動します。今すぐ再起動しますか?",
|
||||
"settings.restart_dialog.restart": "今すぐ再起動",
|
||||
"settings.restart_dialog.later": "後で",
|
||||
"settings.restart_dialog.cancel": "キャンセル",
|
||||
"settings.restart_dock.title": "再起動が必要",
|
||||
"settings.restart_dock.description": "一部の変更はアプリの再起動後に有効になります。",
|
||||
"settings.restart_dock.button": "アプリを再起動",
|
||||
"settings.footer": "LanMountainDesktop 設定",
|
||||
"filepicker.title": "壁紙を選択",
|
||||
"filepicker.image_files": "画像ファイル",
|
||||
"filepicker.video_files": "動画ファイル",
|
||||
"common.day": "昼",
|
||||
"common.night": "夜",
|
||||
"common.back": "戻る",
|
||||
"common.close": "閉じる",
|
||||
"common.unknown": "不明なエラー",
|
||||
"common.recommended": "おすすめ",
|
||||
"common.monet": "Monet",
|
||||
"desktop.page_index_format": "デスクトップ {0}",
|
||||
"launcher.title": "アプリランチャー",
|
||||
"launcher.folder": "フォルダ",
|
||||
"launcher.subtitle": "Windowsスタートメニューからのアプリとフォルダ",
|
||||
"launcher.subtitle_linux": "Linuxデスクトップエントリから発見されたインストール済みアプリ",
|
||||
"launcher.empty": "スタートメニューのエントリが見つかりません。",
|
||||
"launcher.empty_linux": "Linuxデスクトップエントリが見つかりませんでした。",
|
||||
"launcher.empty_folder": "このフォルダは空です。",
|
||||
"launcher.folder_items_format": "{0}個のアプリ",
|
||||
"launcher.context.hide_icon": "アイコンを非表示",
|
||||
"launcher.action.hide": "非表示",
|
||||
"settings.launcher.title": "アプリランチャー",
|
||||
"settings.launcher.description": "アプリランチャーの非表示アプリとフォルダを管理します。",
|
||||
"settings.launcher.hidden_header": "非表示アイテム",
|
||||
"settings.launcher.hidden_desc": "非表示のランチャーエントリを確認し、再度表示します。",
|
||||
"settings.launcher.hidden_hint": "デスクトップ編集モードで、ランチャーアイコンを選択して非表示をクリックします。非表示のエントリはここに表示されます。",
|
||||
"settings.launcher.hidden_empty": "非表示アイテムはありません。",
|
||||
"settings.launcher.hidden_summary_format": "{0}個の非表示アイテム",
|
||||
"settings.launcher.hidden_type_folder": "フォルダ",
|
||||
"settings.launcher.hidden_type_shortcut": "アプリ",
|
||||
"settings.launcher.restore_button": "再表示",
|
||||
"settings.plugins.title": "プラグイン",
|
||||
"settings.plugins.runtime_header": "プラグインランタイム",
|
||||
"settings.plugins.runtime_desc": "プラグインランタイムの状態とロード結果を確認します。",
|
||||
"settings.plugins.runtime_hint": "このページには、インストールされたプラグインの発見ステータス、ロード結果、ランタイム診断が表示されます。",
|
||||
"settings.plugins.runtime_status": "プラグインの発見が完了すると、プラグインランタイムのステータスがここに表示されます。",
|
||||
"settings.plugins.description": "インストールされたプラグインを管理し、ランタイムの状態を確認します。",
|
||||
"settings.plugins.initial_status": "プラグインの状態を更新して、最新のインストール済みプラグインを確認してください。",
|
||||
"settings.plugins.refresh_button": "プラグインを更新",
|
||||
"settings.plugins.refresh_success_installed_format": "{0}個のインストール済みプラグインをロードしました。",
|
||||
"settings.plugins.refresh_success_format": "{0}個のインストール済みプラグインと{1}個のマーケットプレイスエントリをロードしました。",
|
||||
"settings.plugins.refresh_failed": "プラグインカタログインデックスのロードに失敗しました。",
|
||||
"settings.plugins.marketplace_header": "マーケットプレイス",
|
||||
"settings.plugins.marketplace_empty": "現在、マーケットプレイスのプラグインはありません。",
|
||||
"settings.plugins.delete_button_short": "削除",
|
||||
"settings.plugins.install_button_short": "インストール",
|
||||
"settings.plugins.restart_required": "プラグインの変更は再起動後に有効になります。",
|
||||
"settings.plugins.toggle_unchanged_format": "プラグイン「{0}」は変更されませんでした。",
|
||||
"settings.plugins.delete_failed_name_format": "プラグイン「{0}」の削除に失敗しました。",
|
||||
"settings.plugins.install_failed_name_format": "「{0}」のインストールに失敗しました。",
|
||||
"settings.plugins.installed_header": "インストール済みプラグイン",
|
||||
"settings.plugins.installed_desc": "インストール済みプラグインを確認し、ここで削除します。",
|
||||
"settings.plugins.import_header": "パッケージからインストール",
|
||||
"settings.plugins.import_desc": ".laappパッケージを開き、ローカルプラグインディレクトリにステージングします。",
|
||||
"settings.plugins.restart_hint": "プラグインのインストールと削除の変更は、アプリの再起動後に有効になります。",
|
||||
"settings.plugins.empty": "プラグインが見つかりません。",
|
||||
"settings.plugins.runtime_unavailable": "プラグインランタイムは利用できません。",
|
||||
"settings.plugins.summary_format": "{0}個のプラグインを検出; 有効 {1}; ロード済み {2}; 設定ページ {3}; ウィジェット {4}; 失敗 {5}。",
|
||||
"settings.plugins.summary_item_format": "{0} v{1} | {2}",
|
||||
"settings.plugins.state.enabled": "有効",
|
||||
"settings.plugins.state.enabled_failed": "有効 / ロード失敗",
|
||||
"settings.plugins.state.disabled": "無効",
|
||||
"settings.plugins.state.loaded": "ロード済み",
|
||||
"settings.plugins.state.load_failed": "ロード失敗",
|
||||
"settings.plugins.toggle_on": "有効",
|
||||
"settings.plugins.toggle_off": "無効",
|
||||
"settings.plugins.toggle_result_format": "プラグイン「{0}」は次回起動時に{1}になりました。ページとウィジェットの変更を適用するには、アプリを再起動してください。",
|
||||
"settings.plugins.toggle_state_enabled": "有効",
|
||||
"settings.plugins.toggle_state_disabled": "無効",
|
||||
"settings.plugins.toggle_failed_detail_format": "プラグイン「{0}」の更新に失敗しました: {1}",
|
||||
"settings.plugins.install_button": ".laappパッケージを開く",
|
||||
"settings.plugins.install_unavailable": "プラグインランタイムが利用できないため、.laappパッケージをインストールできません。",
|
||||
"settings.plugins.install_hint_format": ".laappパッケージを開いて次にインストールします: {0}",
|
||||
"settings.plugins.install_picker_title": "プラグインパッケージを選択",
|
||||
"settings.plugins.install_file_type": ".laappプラグインパッケージ",
|
||||
"settings.plugins.install_picker_unavailable": "ストレージプロバイダが利用できません。",
|
||||
"settings.plugins.install_copy_failed": "選択した.laappパッケージのコピーに失敗しました。",
|
||||
"settings.plugins.install_success_format": "プラグイン「{0}」がインストールされました。新しく追加された設定ページとウィジェットを適用するには、アプリを再起動してください。",
|
||||
"settings.plugins.install_failed_format": "プラグインパッケージのインストールに失敗しました: {0}",
|
||||
"settings.plugins.delete_button": "プラグインを削除",
|
||||
"settings.plugins.delete_success_format": "プラグイン「{0}」は削除のためにステージングされました。削除を完了するには、アプリを再起動してください。",
|
||||
"settings.plugins.delete_failed_format": "プラグインの削除に失敗しました: {0}",
|
||||
"settings.plugins.delete_failed_detail_format": "プラグイン「{0}」の削除に失敗しました: {1}",
|
||||
"settings.plugins.publisher_format": "パブリッシャー: {0}",
|
||||
"settings.plugins.publisher_unknown": "不明なパブリッシャー",
|
||||
"settings.plugins.source_package": ".laappパッケージ",
|
||||
"settings.plugins.source_manifest": "ルーズマニフェスト",
|
||||
"settings.plugins.subtitle_format": "{0} | {1} | {2}",
|
||||
"settings.plugins.detail_format": "設定ページ: {0} | ウィジェット: {1}",
|
||||
"settings.nav.plugin_catalog": "プラグインカタログ",
|
||||
"settings.plugin_catalog.title": "プラグインカタログ",
|
||||
"settings.plugin_catalog.subtitle": "公式LanAirAppソースからプラグインを参照し、インストールをステージングします。",
|
||||
"settings.plugin_catalog.unavailable": "プラグインランタイムが利用できないため、公式カタログを開けません。",
|
||||
"settings.update.status_idle": "アップデートの確認はまだ実行されていません。",
|
||||
"settings.update.status_preferences_saved": "アップデート設定が保存されました。",
|
||||
"settings.update.status_check_failed": "アップデートの確認に失敗しました。",
|
||||
"settings.update.status_available_summary_format": "アップデートあり: {0}(現在: {1})",
|
||||
"settings.update.status_up_to_date_format": "最新版です({0})。",
|
||||
"settings.window.drawer_default": "詳細",
|
||||
"market.toolbar.search_placeholder": "プラグインを検索",
|
||||
"market.toolbar.refresh": "更新",
|
||||
"market.status.loading": "公式プラグインカタログをロード中...",
|
||||
"market.status.loaded_network_format": "公式ソースから{0}個のプラグインをロードしました。",
|
||||
"market.status.loaded_cache_format": "公式ソースが利用できません。キャッシュから{0}個のプラグインをロードしました。理由: {1}",
|
||||
"market.status.load_failed_format": "プラグインカタログのロードに失敗しました: {0}",
|
||||
"market.status.installing_format": "プラグイン「{0}」をダウンロードしてステージング中...",
|
||||
"market.status.install_success_format": "プラグイン「{0}」がステージングされました。適用するにはアプリを再起動してください。",
|
||||
"market.status.install_failed_format": "プラグインのインストールに失敗しました: {0}",
|
||||
"market.status.host_incompatible_format": "このホストは古すぎます。バージョン{0}以降が必要です。",
|
||||
"market.list.empty": "プラグインカタログはまだロードされていません。",
|
||||
"market.list.no_results": "現在の検索に一致するプラグインはありません。",
|
||||
"market.card.subtitle_format": "{0} | v{1}",
|
||||
"market.card.loaded": "ロード済み",
|
||||
"market.card.pending_restart": "再起動が必要",
|
||||
"market.detail.placeholder": "左側のプラグインを選択して詳細を確認します。",
|
||||
"market.detail.author": "パブリッシャー",
|
||||
"market.detail.version": "バージョン",
|
||||
"market.detail.api_version": "APIバージョン",
|
||||
"market.detail.min_host_version": "最小ホストバージョン",
|
||||
"market.detail.installed_version": "インストール済みバージョン",
|
||||
"market.detail.not_installed": "未インストール",
|
||||
"market.detail.readme": "README",
|
||||
"market.detail.plugin_information": "プラグイン情報",
|
||||
"market.detail.author_subtitle_format": "{0}作成",
|
||||
"market.detail.package_size": "パッケージサイズ",
|
||||
"market.detail.published_at": "公開日",
|
||||
"market.detail.updated_at": "更新日",
|
||||
"market.detail.tags": "タグ",
|
||||
"market.detail.project": "プロジェクト",
|
||||
"market.detail.state": "インストール状態",
|
||||
"market.detail.market_source": "マーケットソース",
|
||||
"market.detail.homepage": "ホームページ",
|
||||
"market.detail.repository": "リポジトリ",
|
||||
"market.detail.release_notes": "リリースノート",
|
||||
"market.detail.dependencies": "依存関係",
|
||||
"market.detail.dependencies_empty": "このプラグインは共有コントラクトの依存関係を宣言していません。",
|
||||
"market.detail.readme_loading": "READMEをロード中...",
|
||||
"market.detail.readme_empty": "READMEは空です。",
|
||||
"market.detail.readme_error_format": "READMEをロードできませんでした: {0}",
|
||||
"market.detail.state.not_installed": "未インストール",
|
||||
"market.detail.state.update_available": "アップデートあり",
|
||||
"market.detail.state.installed": "インストール済み",
|
||||
"market.detail.unknown": "不明",
|
||||
"market.button.install": "インストール",
|
||||
"market.button.update": "アップデート",
|
||||
"market.button.installed": "インストール済み",
|
||||
"market.button.installing": "インストール中...",
|
||||
"market.button.restart": "再起動して適用",
|
||||
"button.component_library": "デスクトップを編集",
|
||||
"tooltip.component_library": "デスクトップを編集",
|
||||
"component_library.title": "ウィジェット",
|
||||
"component_library.empty": "スワイプしてカテゴリを選択し、タップして開き、ウィジェットをデスクトップにドラッグします。",
|
||||
"component_library.drag_hint": "ドラッグして配置",
|
||||
"component.delete": "削除",
|
||||
"component.edit": "編集",
|
||||
"component.editor.instance_scope": "変更はこのコンポーネントインスタンスにのみ適用されます。",
|
||||
"component.editor.info_header": "コンポーネント情報",
|
||||
"component.editor.id_label": "コンポーネントID",
|
||||
"component.editor.placement_label": "配置ID",
|
||||
"component.editor.scope_label": "スコープ",
|
||||
"component.editor.scope_instance": "インスタンススコープのエディタ",
|
||||
"component_category.clock": "時計",
|
||||
"component_category.date": "カレンダー",
|
||||
"component_category.weather": "天気",
|
||||
"component_category.board": "ボード",
|
||||
"component_category.media": "メディア",
|
||||
"component_category.info": "情報",
|
||||
"component_category.calculator": "計算機",
|
||||
"component_category.study": "学習",
|
||||
"component_category.file": "ファイル",
|
||||
"component.date": "カレンダー",
|
||||
"component.month_calendar": "月間カレンダー",
|
||||
"component.lunar_calendar": "旧暦カレンダー",
|
||||
"component.desktop_clock": "時計",
|
||||
"component.weather_clock": "天気時計",
|
||||
"component.world_clock": "世界時計",
|
||||
"component.desktop_timer": "タイマー",
|
||||
"component.desktop_weather": "天気",
|
||||
"component.hourly_weather": "時間別天気",
|
||||
"component.multiday_weather": "数日間天気",
|
||||
"component.extended_weather": "拡張天気",
|
||||
"component.class_schedule": "時間割",
|
||||
"component.music_control": "音楽コントロール",
|
||||
"component.audio_recorder": "レコーダー",
|
||||
"component.daily_poetry": "今日の詩",
|
||||
"component.daily_artwork": "今日のアート",
|
||||
"component.daily_word": "今日の言葉",
|
||||
"component.daily_word_2x2": "今日の言葉 2x2",
|
||||
"component.cnr_daily_news": "CNRヘッドライン",
|
||||
"component.ifeng_news": "iFengニュース",
|
||||
"component.bilibili_hot_search": "Bilibiliトレンド",
|
||||
"component.baidu_hot_search": "Baiduトレンド",
|
||||
"component.stcn24_forum": "STCN 24",
|
||||
"component.exchange_rate_converter": "為替レート変換",
|
||||
"component.whiteboard": "黒板(縦向き)",
|
||||
"component.blackboard_landscape": "黒板(横向き)",
|
||||
"component.browser": "ブラウザ",
|
||||
"component.office_recent_documents": "最近のドキュメント",
|
||||
"whiteboard.settings.desc": "各黒板は独自のノート履歴を保持し、独立して保存します。",
|
||||
"whiteboard.settings.retention.title": "ノートの保持期間",
|
||||
"whiteboard.settings.retention.desc": "この黒板が保存されたノートを保持する期間を選択します。期限切れのデータは自動的に削除されます。",
|
||||
"whiteboard.settings.retention.option": "{0}日",
|
||||
"whiteboard.settings.instance_scope": "この保持設定は黒板コンポーネントインスタンスごとに保存されます。",
|
||||
"office_recent_documents.settings.desc": "このウィジェットが最近のドキュメントをスキャンするWindowsとOfficeのソースを選択します。",
|
||||
"office_recent_documents.settings.sources_title": "最近のドキュメントソース",
|
||||
"office_recent_documents.settings.sources_desc": "複数のソースを組み合わせることができます。レジストリ選択は、Office相互運用MRUフォールバックも利用可能にします。",
|
||||
"office_recent_documents.settings.source.registry": "OfficeレジストリMRU",
|
||||
"office_recent_documents.settings.source.recent_folders": "Windowsの最近使ったフォルダ",
|
||||
"office_recent_documents.settings.source.jump_lists": "Windowsジャンプリスト",
|
||||
"office_recent_documents.settings.hint": "すべてのソースを無効にすると、少なくとも1つのソースが再度有効になるまで、このウィジェットは空のままになります。",
|
||||
"component.removable_storage": "リムーバブルストレージ",
|
||||
"component.holiday_calendar": "祝日カレンダー",
|
||||
"component.study_environment": "環境",
|
||||
"component.study_session_control": "学習セッション制御",
|
||||
"component.study_session_history": "セッション履歴",
|
||||
"component.study_noise_curve": "ノイズカーブ",
|
||||
"component.study_noise_distribution": "ノイズ分布",
|
||||
"component.study_score_overview": "学習スコア概要",
|
||||
"component.study_deduction_reasons": "減点理由",
|
||||
"component.study_interrupt_density": "中断密度",
|
||||
"desktop_clock.settings.title": "時計の設定",
|
||||
"desktop_clock.settings.desc": "単一時計のタイムゾーンを選択します。",
|
||||
"desktop_clock.settings.timezone_label": "タイムゾーン",
|
||||
"desktop_clock.settings.second_mode_label": "秒針",
|
||||
"clock.second_mode.tick": "ティック",
|
||||
"clock.second_mode.sweep": "スイープ",
|
||||
"poetry.widget.loading_content": "詩を読み込み中...",
|
||||
"poetry.widget.loading_author": "読み込み中...",
|
||||
"poetry.widget.fetch_failed": "詩の取得に失敗しました",
|
||||
"poetry.widget.fallback_content": "今日の詩は一時的に利用できません。",
|
||||
"poetry.widget.fallback_author": "後でもう一度お試しください",
|
||||
"poetry.widget.unknown_author": "不明",
|
||||
"artwork.widget.loading": "読み込み中...",
|
||||
"artwork.widget.loading_title": "今日のアート",
|
||||
"artwork.widget.loading_subtitle": "今日の傑作を取得中",
|
||||
"artwork.widget.fetch_failed": "アートの取得に失敗しました",
|
||||
"artwork.widget.fallback_title": "今日のアート",
|
||||
"artwork.widget.fallback_artist": "おすすめサービスは利用できません",
|
||||
"artwork.widget.fallback_year": "後でもう一度お試しください",
|
||||
"artwork.widget.unknown_artist": "不明なアーティスト",
|
||||
"dailyword.widget.loading": "読み込み中...",
|
||||
"dailyword.widget.loading_word": "今日の言葉",
|
||||
"dailyword.widget.loading_pronunciation": "発音を取得中...",
|
||||
"dailyword.widget.loading_meaning": "意味を取得中...",
|
||||
"dailyword.widget.loading_example": "例文を取得中...",
|
||||
"dailyword.widget.loading_example_translation": "読み込み中...",
|
||||
"dailyword.widget.fetch_failed": "今日の言葉の取得に失敗しました",
|
||||
"dailyword.widget.fallback_word": "今日の言葉",
|
||||
"dailyword.widget.fallback_pronunciation": "発音は利用できません",
|
||||
"dailyword.widget.fallback_meaning": "Youdao辞書は一時的に利用できません。",
|
||||
"dailyword.widget.fallback_example": "更新ボタンをタップして再試行してください。",
|
||||
"dailyword.widget.fallback_example_translation": "ネットワークが回復すると再試行します。",
|
||||
"dailyword2x2.widget.tap_to_show": "タップして意味を表示",
|
||||
"cnrnews.widget.loading": "読み込み中...",
|
||||
"cnrnews.widget.loading_title": "CNRヘッドラインを取得中",
|
||||
"cnrnews.widget.loading_subtitle": "お待ちください",
|
||||
"cnrnews.widget.fetch_failed": "ニュースの取得に失敗しました",
|
||||
"cnrnews.widget.fallback_title": "CNRニュースは一時的に利用できません",
|
||||
"cnrnews.widget.fallback_subtitle": "更新をタップして再試行してください",
|
||||
"cnrnews.widget.hot_label": "ホット",
|
||||
"bilihot.widget.brand": "Bilibiliトレンド",
|
||||
"bilihot.widget.top_right_label": "Bilibiliトレンド",
|
||||
"bilihot.widget.search_entry": "検索",
|
||||
"bilihot.widget.search_placeholder": "トレンドトピックを検索",
|
||||
"bilihot.widget.loading": "読み込み中...",
|
||||
"bilihot.widget.loading_item": "読み込み中...",
|
||||
"bilihot.widget.fetch_failed": "トレンドの取得に失敗しました",
|
||||
"bilihot.widget.fallback_item": "トレンドデータなし",
|
||||
"bilihot.widget.more_hot": "もっとトレンドを見る",
|
||||
"baiduhot.widget.brand": "Baiduトレンド",
|
||||
"baiduhot.widget.loading": "読み込み中...",
|
||||
"baiduhot.widget.loading_item": "読み込み中...",
|
||||
"baiduhot.widget.fetch_failed": "トレンドの取得に失敗しました",
|
||||
"baiduhot.widget.fallback_item": "トレンドデータなし",
|
||||
"baiduhot.widget.refresh_tooltip": "更新",
|
||||
"ifeng.widget.brand": "iFengニュース",
|
||||
"ifeng.widget.loading": "読み込み中...",
|
||||
"ifeng.widget.loading_item": "読み込み中...",
|
||||
"ifeng.widget.fetch_failed": "ニュースの取得に失敗しました",
|
||||
"ifeng.widget.fallback_item": "ニュースデータなし",
|
||||
"ifeng.widget.refresh_tooltip": "更新",
|
||||
"dailyword.settings.title": "今日の言葉の設定",
|
||||
"dailyword.settings.desc": "自動更新と更新間隔を設定します。",
|
||||
"dailyword.settings.auto_refresh_label": "自動更新",
|
||||
"dailyword.settings.auto_refresh_enabled": "自動更新を有効にする",
|
||||
"dailyword.settings.frequency_label": "更新間隔",
|
||||
"bilihot.settings.title": "Bilibiliトレンドの設定",
|
||||
"bilihot.settings.desc": "自動更新と更新間隔を設定します。",
|
||||
"bilihot.settings.auto_refresh_label": "自動更新",
|
||||
"bilihot.settings.auto_refresh_enabled": "自動更新を有効にする",
|
||||
"bilihot.settings.frequency_label": "更新間隔",
|
||||
"baiduhot.settings.title": "Baiduトレンドの設定",
|
||||
"baiduhot.settings.desc": "ソース、自動更新、更新間隔を設定します。",
|
||||
"baiduhot.settings.source_label": "データソース",
|
||||
"baiduhot.settings.source_official": "公式ソース",
|
||||
"baiduhot.settings.source_rss": "サードパーティRSS",
|
||||
"baiduhot.settings.auto_refresh_label": "自動更新",
|
||||
"baiduhot.settings.auto_refresh_enabled": "自動更新を有効にする",
|
||||
"baiduhot.settings.frequency_label": "更新間隔",
|
||||
"ifeng.settings.title": "iFengニュースの設定",
|
||||
"ifeng.settings.desc": "チャンネル、自動更新、更新間隔を設定します。",
|
||||
"ifeng.settings.channel_label": "ニュースチャンネル",
|
||||
"ifeng.settings.channel_comprehensive": "総合",
|
||||
"ifeng.settings.channel_mainland": "中国本土",
|
||||
"ifeng.settings.channel_taiwan": "台湾",
|
||||
"ifeng.settings.auto_refresh_label": "自動更新",
|
||||
"ifeng.settings.auto_refresh_enabled": "自動更新を有効にする",
|
||||
"ifeng.settings.frequency_label": "更新間隔",
|
||||
"refresh.frequency.5m": "5分",
|
||||
"refresh.frequency.10m": "10分",
|
||||
"refresh.frequency.12m": "12分",
|
||||
"refresh.frequency.15m": "15分",
|
||||
"refresh.frequency.20m": "20分",
|
||||
"refresh.frequency.30m": "30分",
|
||||
"refresh.frequency.40m": "40分",
|
||||
"refresh.frequency.1h": "1時間",
|
||||
"refresh.frequency.3h": "3時間",
|
||||
"refresh.frequency.6h": "6時間",
|
||||
"refresh.frequency.12h": "12時間",
|
||||
"refresh.frequency.24h": "24時間",
|
||||
"weather.widget.settings.title": "天気ウィジェットの設定",
|
||||
"weather.widget.settings.desc": "すべての天気ウィジェットの自動更新と更新間隔を設定します。",
|
||||
"weather.widget.settings.auto_refresh_label": "自動更新",
|
||||
"weather.widget.settings.auto_refresh_enabled": "自動更新を有効にする",
|
||||
"weather.widget.settings.frequency_label": "更新間隔",
|
||||
"weather.widget.settings.frequency_10m": "10分",
|
||||
"weather.widget.settings.frequency_12m": "12分",
|
||||
"weather.widget.settings.frequency_15m": "15分",
|
||||
"weather.widget.settings.frequency_30m": "30分",
|
||||
"weather.widget.settings.frequency_1h": "1時間",
|
||||
"weather.widget.settings.frequency_3h": "3時間",
|
||||
"stcn24.widget.loading": "読み込み中...",
|
||||
"stcn24.widget.loading_item": "読み込み中...",
|
||||
"stcn24.widget.fetch_failed": "フォーラム投稿の取得に失敗しました",
|
||||
"stcn24.widget.fallback_item": "投稿なし",
|
||||
"stcn24.settings.title": "STCN 24の設定",
|
||||
"stcn24.settings.desc": "情報ソース、自動更新、更新間隔を設定します。",
|
||||
"stcn24.settings.source_label": "情報ソース",
|
||||
"stcn24.settings.source_latest_created": "最新の投稿",
|
||||
"stcn24.settings.source_latest_activity": "最新のアクティビティ",
|
||||
"stcn24.settings.source_most_replies": "返信数順",
|
||||
"stcn24.settings.source_earliest_created": "最古の投稿",
|
||||
"stcn24.settings.source_earliest_activity": "最古のアクティビティ",
|
||||
"stcn24.settings.source_least_replies": "返信が少ない順",
|
||||
"stcn24.settings.source_frontpage_latest": "フロントページ最新",
|
||||
"stcn24.settings.source_frontpage_earliest": "フロントページ最古",
|
||||
"stcn24.settings.auto_refresh_label": "自動更新",
|
||||
"stcn24.settings.auto_refresh_enabled": "自動更新を有効にする",
|
||||
"stcn24.settings.frequency_label": "更新間隔",
|
||||
"stcn24.settings.frequency_5m": "5分",
|
||||
"stcn24.settings.frequency_10m": "10分",
|
||||
"stcn24.settings.frequency_20m": "20分",
|
||||
"stcn24.settings.frequency_30m": "30分",
|
||||
"stcn24.settings.frequency_1h": "1時間",
|
||||
"stcn24.settings.frequency_3h": "3時間",
|
||||
"exchange.widget.loading": "為替レートを読み込み中...",
|
||||
"exchange.widget.fetch_failed": "為替レートの取得に失敗しました",
|
||||
"cnrnews.settings.title": "CNRの設定",
|
||||
"cnrnews.settings.desc": "自動ローテーションと更新間隔を設定します。",
|
||||
"cnrnews.settings.auto_rotate_label": "自動ローテーション",
|
||||
"cnrnews.settings.auto_rotate_enabled": "自動ローテーションを有効にする",
|
||||
"cnrnews.settings.frequency_label": "ローテーション間隔",
|
||||
"cnrnews.settings.frequency_5m": "5分",
|
||||
"cnrnews.settings.frequency_10m": "10分",
|
||||
"cnrnews.settings.frequency_40m": "40分",
|
||||
"cnrnews.settings.frequency_1h": "1時間",
|
||||
"cnrnews.settings.frequency_12h": "12時間",
|
||||
"cnrnews.settings.frequency_24h": "24時間",
|
||||
"artwork.settings.title": "今日のアートの設定",
|
||||
"artwork.settings.desc": "今日のアートで使用されるデータソースを切り替えます。",
|
||||
"artwork.settings.source_label": "ミラーソース",
|
||||
"artwork.settings.source_domestic": "国内ミラー",
|
||||
"artwork.settings.source_overseas": "海外ミラー",
|
||||
"artwork.settings.source_status_domestic": "現在のソース: 国内ミラー(中国ネットワーク向けに最適化)",
|
||||
"artwork.settings.source_status_overseas": "現在のソース: 海外ミラー(美術館のおすすめ)",
|
||||
"music.widget.unsupported": "このプラットフォームでは音楽コントロールはサポートされていません",
|
||||
"music.widget.unsupported_hint": "このウィジェットにはWindows SMTCが必要です",
|
||||
"music.widget.no_session": "音楽ソースなし",
|
||||
"music.widget.no_session_hint": "アプリストアからQQ音楽/酷狗/網易雲音楽をインストールしてください",
|
||||
"music.widget.open_player": "プレーヤーを開く",
|
||||
"music.widget.unknown_title": "不明なタイトル",
|
||||
"music.widget.unknown_artist": "不明なアーティスト",
|
||||
"music.widget.status.opened": "開かれました",
|
||||
"music.widget.status.changing": "変更中",
|
||||
"music.widget.status.stopped": "停止",
|
||||
"music.widget.status.playing": "再生中",
|
||||
"music.widget.status.paused": "一時停止",
|
||||
"recording.widget.title": "レコーダー",
|
||||
"recording.widget.hint.ready": "赤いボタンをタップして録音",
|
||||
"recording.widget.hint.recording": "録音中",
|
||||
"recording.widget.hint.paused": "一時停止",
|
||||
"recording.widget.hint.unsupported": "マイクが利用できません",
|
||||
"recording.widget.hint.error": "録音に失敗しました",
|
||||
"recording.widget.hint.saved_format": "保存しました {0}",
|
||||
"recording.widget.save_picker_title": "録音ファイルを保存",
|
||||
"recording.widget.save_picker_type": "WAVオーディオ",
|
||||
"study.environment.status_label": "環境",
|
||||
"study.environment.status.initializing": "初期化中",
|
||||
"study.environment.status.ready": "準備完了",
|
||||
"study.environment.status.quiet": "静か",
|
||||
"study.environment.status.noisy": "うるさい",
|
||||
"study.environment.status.paused": "一時停止",
|
||||
"study.environment.status.error": "エラー",
|
||||
"study.environment.status.unsupported": "未対応",
|
||||
"study.environment.value.unavailable": "--",
|
||||
"study.environment.value.display_format": "{0:F1} dB",
|
||||
"study.environment.value.dbfs_format": "{0:F1} dBFS",
|
||||
"study.environment.settings.title": "環境ウィジェットの設定",
|
||||
"study.environment.settings.desc": "右側のリアルタイムノイズ値表示を設定します。",
|
||||
"study.environment.settings.show_display_db": "表示dBを表示",
|
||||
"study.environment.settings.show_dbfs": "dBFSを表示",
|
||||
"study.environment.settings.hint": "少なくとも1つの表示モードを有効にしておく必要があります。",
|
||||
"removable_storage.settings.desc": "接続されたUSBドライブを表示し、クイックオープンと取り出しアクションを提供します。",
|
||||
"removable_storage.settings.behavior_title": "動作",
|
||||
"removable_storage.settings.behavior_desc": "ウィジェットはリムーバブルドライブを自動的に監視し、最新の挿入されたUSBドライブに切り替わります。",
|
||||
"removable_storage.action.open": "開く",
|
||||
"removable_storage.action.eject": "取り出し",
|
||||
"removable_storage.widget.default_name": "リムーバブルドライブ",
|
||||
"removable_storage.widget.empty_title": "デバイスが挿入されていません",
|
||||
"removable_storage.widget.empty_subtitle": "USBドライブを挿入してここに表示します。",
|
||||
"removable_storage.widget.empty_hint": "リムーバブルデバイスが挿入されるまで、ボタンは無効のままです。",
|
||||
"removable_storage.widget.ready": "開くか取り出す準備ができました。",
|
||||
"removable_storage.widget.ejecting": "ドライブを取り出し中...",
|
||||
"removable_storage.widget.eject_failed": "このドライブを取り出せませんでした。上のファイルを閉じて再試行してください。",
|
||||
"removable_storage.widget.open_failed": "このドライブを開けませんでした。",
|
||||
"removable_storage.widget.refresh_failed": "ドライブリストの更新に失敗しました。",
|
||||
"study.session_control.action.start": "学習セッションを開始",
|
||||
"study.session_control.action.stop": "学習セッションを停止",
|
||||
"study.session_control.idle_hint": "右のボタンをタップして開始",
|
||||
"study.session_control.report_preview": "レポートをプレビュー",
|
||||
"study.session_control.report_confirm_hint": "右のボタンをタップして確認",
|
||||
"study.session_control.running_elapsed_format": "経過 {0}",
|
||||
"study.session_control.last_session_format": "前回 {0}",
|
||||
"study.session_control.start_failed": "セッションを開始できません",
|
||||
"study.session_control.stop_failed": "セッションを停止できません",
|
||||
"study.session_history.title": "セッション履歴",
|
||||
"study.session_history.empty": "セッション履歴なし",
|
||||
"study.session_history.select_failed": "セッションを切り替えられません",
|
||||
"study.session_history.rename_failed": "セッション名を変更できません",
|
||||
"study.session_history.delete_failed": "セッションを削除できません",
|
||||
"study.session_history.rename_placeholder": "セッション名を入力",
|
||||
"study.session_history.rename_confirm": "名前変更を確認",
|
||||
"study.session_history.rename_cancel": "名前変更をキャンセル",
|
||||
"study.session_history.loading": "データを読み込み中...",
|
||||
"study.session_history.loaded": "データが読み込まれました",
|
||||
"study.session_history.duration_format": "{0:hh\\:mm\\:ss}",
|
||||
"study.session_history.meta_format": "{0} · 平均 {1:F1}",
|
||||
"study.session_history.action.view": "表示",
|
||||
"study.session_history.action.rename": "名前変更",
|
||||
"study.session_history.action.delete": "削除",
|
||||
"study.session_history.dialog.rename_title": "セッション名を変更",
|
||||
"study.session_history.dialog.rename_message": "「{0}」の新しい名前を入力してください。",
|
||||
"study.session_history.dialog.delete_title": "セッションを削除",
|
||||
"study.session_history.dialog.delete_message": "「{0}」を削除しますか?これは元に戻せません。",
|
||||
"study.session_history.dialog.delete_confirm": "削除",
|
||||
"study.noise_curve.value_format": "{0:F1} dB",
|
||||
"study.noise_curve.axis.now": "現在",
|
||||
"study.noise_distribution.title": "ノイズレベル分布",
|
||||
"study.noise_distribution.mode.realtime": "リアルタイム",
|
||||
"study.noise_distribution.mode.session": "セッション",
|
||||
"study.noise_distribution.summary.mainly_format": "主に: {0}",
|
||||
"study.noise_distribution.summary.latest_format": "最新: {0}",
|
||||
"study.noise_distribution.summary.compact_format": "主 {0} · 新 {1}",
|
||||
"study.noise_distribution.level.quiet": "静か",
|
||||
"study.noise_distribution.level.normal": "普通",
|
||||
"study.noise_distribution.level.noisy": "うるさい",
|
||||
"study.noise_distribution.level.extreme": "極端",
|
||||
"study.noise_distribution.axis.extreme": "極端",
|
||||
"study.noise_distribution.axis.noisy": "うるさい",
|
||||
"study.noise_distribution.axis.normal": "普通",
|
||||
"study.noise_distribution.axis.quiet": "静か",
|
||||
"study.noise_distribution.axis.now": "現在",
|
||||
"study.score_overview.title": "学習スコア",
|
||||
"study.score_overview.mode.realtime": "リアルタイム",
|
||||
"study.score_overview.mode.session": "セッション",
|
||||
"study.score_overview.current": "現在",
|
||||
"study.score_overview.average": "平均",
|
||||
"study.score_overview.minimum": "最小",
|
||||
"study.score_overview.maximum": "最大",
|
||||
"study.score_overview.average_short": "平均",
|
||||
"study.score_overview.minimum_short": "最小",
|
||||
"study.score_overview.maximum_short": "最大",
|
||||
"study.score_overview.unavailable": "--",
|
||||
"study.deduction.title": "減点理由",
|
||||
"study.deduction.mode.realtime": "リアルタイム",
|
||||
"study.deduction.mode.session": "セッション",
|
||||
"study.deduction.reason.sustained": "持続ノイズ",
|
||||
"study.deduction.reason.time": "閾値超過時間",
|
||||
"study.deduction.reason.segment": "中断頻度",
|
||||
"study.deduction.reason.sustained_short": "持続",
|
||||
"study.deduction.reason.time_short": "時間",
|
||||
"study.deduction.reason.segment_short": "中断",
|
||||
"study.deduction.metric.sustained_format": "p50 {0:F1} dBFS",
|
||||
"study.deduction.metric.sustained_short_format": "p50 {0:F1}",
|
||||
"study.deduction.metric.time_format": "{0:F1}%超過",
|
||||
"study.deduction.metric.time_short_format": "{0:F1}%",
|
||||
"study.deduction.metric.segment_format": "{0:F1}/分",
|
||||
"study.deduction.metric.segment_short_format": "{0:F1}/分",
|
||||
"study.deduction.loss_format": "-{0:F1}",
|
||||
"study.deduction.total_loss_format": "合計 -{0:F1}",
|
||||
"study.deduction.total_score_format": "スコア {0:F1}",
|
||||
"study.deduction.total_loss_unavailable": "合計 {0}",
|
||||
"study.deduction.total_score_unavailable": "スコア {0}",
|
||||
"study.deduction.unavailable": "--",
|
||||
"study.interrupt_density.title": "中断密度",
|
||||
"study.interrupt_density.mode.realtime": "リアルタイム",
|
||||
"study.interrupt_density.mode.session": "セッション",
|
||||
"study.interrupt_density.unit": "/分",
|
||||
"study.interrupt_density.segment_count": "中断回数",
|
||||
"study.interrupt_density.segment_count_short": "回数",
|
||||
"study.interrupt_density.duration": "期間",
|
||||
"study.interrupt_density.duration_short": "時間",
|
||||
"study.interrupt_density.density_value_format": "{0:F1}",
|
||||
"study.interrupt_density.segment_count_value_format": "{0}",
|
||||
"study.interrupt_density.level_format": "レベル {0}",
|
||||
"study.interrupt_density.level.calm": "穏やか",
|
||||
"study.interrupt_density.level.normal": "普通",
|
||||
"study.interrupt_density.level.frequent": "頻繁",
|
||||
"study.interrupt_density.level.severe": "深刻",
|
||||
"study.interrupt_density.threshold_format": "ペナルティ閾値 {0:F1}/分",
|
||||
"study.interrupt_density.unavailable": "--",
|
||||
"desktop.add_page": "ページを追加",
|
||||
"desktop.delete_page": "ページを削除",
|
||||
"placement.fill": "フィル",
|
||||
"placement.fit": "フィット",
|
||||
"placement.stretch": "ストレッチ",
|
||||
"placement.center": "中央",
|
||||
"placement.tile": "タイル",
|
||||
"single_instance.notice.title": "アプリは既に実行中",
|
||||
"single_instance.notice.description": "アプリは既に実行中です。複数回クリックして開く必要はありません。",
|
||||
"single_instance.notice.button": "OK",
|
||||
"market.status.install_success_restart_format": "✓ プラグイン「{0}」が正常にインストールされました!有効にするには、アプリケーションを再起動してください。",
|
||||
"market.dialog.restart_message_format": "プラグイン「{0}」が正常にインストールされました。\n\nこのプラグインを使用するには、今すぐアプリケーションを再起動する必要があります。\n\n再起動しますか?",
|
||||
"component.settings.color_scheme": "カラースキーム"
|
||||
}
|
||||
976
LanMountainDesktop/Localization/ko-KR.json
Normal file
976
LanMountainDesktop/Localization/ko-KR.json
Normal file
@@ -0,0 +1,976 @@
|
||||
{
|
||||
"app.title": "LanMountainDesktop",
|
||||
"tray.tooltip": "LanMountainDesktop",
|
||||
"tray.menu.show_desktop": "바탕화면 열기",
|
||||
"tray.menu.settings": "설정",
|
||||
"tray.menu.component_library": "위젯 라이브러리",
|
||||
"tray.menu.restart": "앱 재시작",
|
||||
"tray.menu.exit": "앱 종료",
|
||||
"button.back_to_windows": "Windows로 돌아가기",
|
||||
"button.back_to_platform": "{0}로 돌아가기",
|
||||
"tooltip.back_to_windows": "Windows로 돌아가기",
|
||||
"tooltip.back_to_platform": "{0}로 돌아가기",
|
||||
"platform.windows": "Windows",
|
||||
"platform.linux": "Linux",
|
||||
"platform.macos": "macOS",
|
||||
"tooltip.open_settings": "설정",
|
||||
"settings.title": "설정",
|
||||
"settings.shell.title": "설정",
|
||||
"settings.shell.subtitle": "LanMountainDesktop 독립 설정 모듈",
|
||||
"settings.shell.sidebar_hint": "카테고리를 선택하여 앱 동작, 바탕화면 레이아웃 및 외관을 조정합니다.",
|
||||
"settings.shell.footer_hint": "트레이에서 열리는 설정은 이 독립 설정 모듈에서 관리됩니다.",
|
||||
"settings.back_to_desktop": "바탕화면으로 돌아가기",
|
||||
"settings.nav_header": "설정",
|
||||
"settings.nav.group_desktop": "바탕화면",
|
||||
"settings.nav.group_system": "시스템",
|
||||
"settings.nav.group_extensions": "확장",
|
||||
"settings.nav.wallpaper": "배경화면",
|
||||
"settings.nav.grid": "컴포넌트",
|
||||
"settings.nav.color": "색상",
|
||||
"settings.nav.status_bar": "상태 표시줄",
|
||||
"settings.nav.weather": "날씨",
|
||||
"settings.nav.region": "지역",
|
||||
"settings.nav.update": "업데이트",
|
||||
"settings.nav.privacy": "개인정보",
|
||||
"settings.nav.launcher": "앱 런처",
|
||||
"settings.nav.plugins": "플러그인",
|
||||
"settings.nav.about": "정보",
|
||||
"settings.wallpaper.title": "배경화면",
|
||||
"settings.wallpaper.description": "이미지 또는 비디오를 선택하여 앱 창의 배경화면으로 즉시 적용합니다.",
|
||||
"settings.wallpaper.current_label": "현재 배경화면",
|
||||
"settings.wallpaper.type_label": "배경화면 유형",
|
||||
"settings.wallpaper.type.image": "이미지",
|
||||
"settings.wallpaper.type.solid_color": "단색",
|
||||
"settings.wallpaper.type.system": "시스템 배경화면",
|
||||
"settings.wallpaper.system.label": "시스템 배경화면",
|
||||
"settings.wallpaper.system.unavailable": "시스템 배경화면을 불러올 수 없습니다",
|
||||
"settings.wallpaper.refresh_interval": "새로고침 간격",
|
||||
"settings.wallpaper.refresh_now": "지금 새로고침",
|
||||
"settings.wallpaper.refresh.30s": "30초",
|
||||
"settings.wallpaper.refresh.1m": "1분",
|
||||
"settings.wallpaper.refresh.5m": "5분",
|
||||
"settings.wallpaper.refresh.10m": "10분",
|
||||
"settings.wallpaper.refresh.15m": "15분",
|
||||
"settings.wallpaper.refresh.30m": "30분",
|
||||
"settings.wallpaper.refresh.1h": "1시간",
|
||||
"settings.wallpaper.refresh.2h": "2시간",
|
||||
"settings.wallpaper.refresh.4h": "4시간",
|
||||
"settings.wallpaper.refresh.8h": "8시간",
|
||||
"settings.wallpaper.refresh.12h": "12시간",
|
||||
"settings.wallpaper.refresh.24h": "24시간",
|
||||
"settings.wallpaper.color_label": "배경화면 색상",
|
||||
"settings.wallpaper.placement_label": "배치",
|
||||
"settings.wallpaper.placement_desc": "이미지가 바탕화면에 표시되는 방식을 조정합니다.",
|
||||
"settings.wallpaper.pick_button": "파일 찾아보기",
|
||||
"settings.wallpaper.clear_button": "단색으로 재설정",
|
||||
"settings.wallpaper.no_selection": "배경화면이 선택되지 않았습니다.",
|
||||
"settings.wallpaper.storage_unavailable": "저장소 제공자를 사용할 수 없습니다.",
|
||||
"settings.wallpaper.import_failed": "배경화면 파일 가져오기에 실패했습니다.",
|
||||
"settings.wallpaper.image_applied": "이미지 배경화면이 적용되었습니다.",
|
||||
"settings.wallpaper.video_applied": "비디오 배경화면이 적용되었습니다.",
|
||||
"settings.wallpaper.unsupported_file": "선택한 파일 형식은 지원되지 않습니다.",
|
||||
"settings.wallpaper.apply_failed_format": "배경화면 적용 실패: {0}",
|
||||
"settings.wallpaper.mode_format": "배경화면 모드: {0}.",
|
||||
"settings.wallpaper.video_mode": "비디오 배경화면은 자동 채우기 모드를 사용합니다.",
|
||||
"settings.wallpaper.cleared": "배경이 단색으로 재설정되었습니다.",
|
||||
"settings.wallpaper.default_status": "현재 배경은 단색을 사용합니다.",
|
||||
"settings.wallpaper.saved_not_found": "저장된 배경화면 파일을 찾을 수 없습니다. 단색 배경을 사용합니다.",
|
||||
"settings.wallpaper.restored": "저장된 설정에서 배경화면이 복원되었습니다.",
|
||||
"settings.wallpaper.video_restored": "저장된 설정에서 비디오 배경화면이 복원되었습니다.",
|
||||
"settings.wallpaper.restore_failed": "저장된 배경화면 복원에 실패했습니다. 단색 배경을 사용합니다.",
|
||||
"settings.wallpaper.video_not_found": "비디오 배경화면 파일을 찾을 수 없습니다.",
|
||||
"settings.wallpaper.video_player_unavailable": "비디오 플레이어를 사용할 수 없습니다.",
|
||||
"settings.wallpaper.video_play_failed_format": "비디오 배경화면 재생 실패: {0}",
|
||||
"settings.grid.title": "그리드 레이아웃",
|
||||
"settings.grid.description": "모든 컴포넌트는 최소 하나의 셀을 차지해야 합니다 (최소 1x1).",
|
||||
"settings.grid.short_side_label": "짧은 쪽 셀 수",
|
||||
"settings.grid.spacing_label": "그리드 간격",
|
||||
"settings.grid.spacing_relaxed": "여유 있음 (iOS)",
|
||||
"settings.grid.spacing_compact": "컴팩트 (Android)",
|
||||
"settings.grid.edge_inset_label": "화면 여백",
|
||||
"settings.grid.edge_inset_px_format": "≈ {0:F1}px",
|
||||
"settings.grid.apply_button": "적용",
|
||||
"settings.grid.info_format": "그리드: {0}열 x {1}행 | 셀 {2:F1}px (1:1)",
|
||||
"settings.color.title": "색상",
|
||||
"settings.color.description": "주야간 모드를 전환하고 앱 강조 색상을 선택합니다.",
|
||||
"settings.color.day_night_label": "주야간 모드",
|
||||
"settings.color.day_night_on": "야간",
|
||||
"settings.color.day_night_off": "주간",
|
||||
"settings.color.recommended_label": "추천 색상",
|
||||
"settings.color.system_monet_label": "시스템 Monet 색상",
|
||||
"settings.color.refresh_button": "새로고침",
|
||||
"settings.color.mode_night": "야간 모드 활성화됨",
|
||||
"settings.color.mode_day": "주간 모드 활성화됨",
|
||||
"settings.color.mode_status_format": "테마 모드: {0}.",
|
||||
"settings.color.monet_refreshed": "Monet 색상이 새로고침되었습니다.",
|
||||
"settings.color.theme_ready_format": "테마 색상 준비됨: {0}.",
|
||||
"settings.color.theme_applied_format": "{0} 테마 색상 적용됨: {1}.",
|
||||
"settings.color.theme_updated_wallpaper": "배경화면이 업데이트되어 Monet 색상이 새로고침되었습니다.",
|
||||
"settings.color.theme_cleared_wallpaper": "배경화면이 제거되어 Monet 색상이 새로고침되었습니다.",
|
||||
"settings.status_bar.title": "상태 표시줄",
|
||||
"settings.status_bar.description": "상단 상태 표시줄에 표시할 컴포넌트를 선택합니다.",
|
||||
"settings.status_bar.clock_header": "시계 컴포넌트",
|
||||
"settings.status_bar.clock_description": "상단 상태 표시줄에 시계를 표시합니다.",
|
||||
"settings.status_bar.clock_transparent_background_label": "투명 배경",
|
||||
"settings.status_bar.clock_transparent_background_desc": "캡슐 배경을 제거하고 시계 텍스트만 유지합니다.",
|
||||
"settings.status_bar.spacing_header": "컴포넌트 간격",
|
||||
"settings.status_bar.spacing_desc": "상태 표시줄 컴포넌트 사이의 간격을 조정합니다.",
|
||||
"settings.status_bar.spacing_mode_compact": "컴팩트",
|
||||
"settings.status_bar.spacing_mode_relaxed": "여유 있음",
|
||||
"settings.status_bar.spacing_mode_custom": "사용자 지정",
|
||||
"settings.status_bar.spacing_custom_label": "사용자 지정 간격 (%)",
|
||||
"settings.status_bar.spacing_custom_px_format": "≈ {0:F1}px",
|
||||
"settings.privacy.title": "개인정보",
|
||||
"settings.privacy.description": "선택적 익명 업로드 설정을 관리하여 앱 경험을 점진적으로 개선하는 데 도움을 줍니다.",
|
||||
"settings.privacy.crash_upload_title": "익명 충돌 데이터 업로드",
|
||||
"settings.privacy.crash_upload_description": "앱 안정성 향상에 도움을 줍니다.",
|
||||
"settings.privacy.usage_upload_title": "익명 사용 데이터 업로드",
|
||||
"settings.privacy.usage_upload_description": "앱 기능 개선에 도움을 줍니다.",
|
||||
"settings.privacy.device_id_title": "기기 식별자",
|
||||
"settings.privacy.device_id_description": "이 기기의 고유 식별자입니다. 새로고침을 클릭하여 재생성합니다.",
|
||||
"settings.privacy.refresh_device_id": "새로고침",
|
||||
"settings.privacy.policy_hint_prefix": "자세한 내용은",
|
||||
"settings.privacy.view_policy": "개인정보 처리방침 보기",
|
||||
"settings.weather.title": "날씨",
|
||||
"settings.weather.description": "날씨 위치, Xiaomi 날씨 미리보기 및 시작 시 위치 새로고침 동작을 구성합니다.",
|
||||
"settings.weather.location_source_header": "위치 소스",
|
||||
"settings.weather.location_source_desc": "날씨 컴포넌트가 현재 위치를 해석하는 방법을 선택합니다.",
|
||||
"settings.weather.mode_city_search": "도시 검색",
|
||||
"settings.weather.mode_coordinates": "좌표 입력",
|
||||
"settings.weather.auto_refresh": "시작 시 위치 자동 새로고침",
|
||||
"settings.weather.city_search_header": "도시 검색",
|
||||
"settings.weather.city_search_desc": "도시를 검색하고 날씨 위치를 적용합니다.",
|
||||
"settings.weather.search_placeholder": "예: 서울",
|
||||
"settings.weather.search_button": "검색",
|
||||
"settings.weather.apply_city_button": "도시 적용",
|
||||
"settings.weather.search_hint": "도시 이름을 입력하여 검색한 후 결과를 적용합니다.",
|
||||
"settings.weather.search_required": "먼저 도시 키워드를 입력하세요.",
|
||||
"settings.weather.search_no_results": "일치하는 위치를 찾을 수 없습니다.",
|
||||
"settings.weather.search_failed_format": "검색 실패: {0}",
|
||||
"settings.weather.search_result_count_format": "총 {0}개 위치를 찾았습니다.",
|
||||
"settings.weather.search_select_required": "먼저 검색 결과에서 위치를 선택하세요.",
|
||||
"settings.weather.search_applied_format": "위치 적용됨: {0}",
|
||||
"settings.weather.coordinates_header": "좌표 입력",
|
||||
"settings.weather.coordinates_desc": "위도와 경도를 설정하고 선택적으로 위치 키와 표시 이름을 입력합니다.",
|
||||
"settings.weather.latitude_label": "위도",
|
||||
"settings.weather.longitude_label": "경도",
|
||||
"settings.weather.location_key_placeholder": "위치 키 (선택)",
|
||||
"settings.weather.location_name_placeholder": "표시 이름 (선택)",
|
||||
"settings.weather.apply_coordinates_button": "좌표 적용",
|
||||
"settings.weather.coordinates_saved_format": "좌표 저장됨: {0:F4}, {1:F4}",
|
||||
"settings.weather.coordinates_default_name_format": "좌표 {0:F4}, {1:F4}",
|
||||
"settings.weather.location_services_header": "위치 서비스",
|
||||
"settings.weather.location_services_desc": "현재 Windows 위치를 사용하고 시작 시 날씨 위치를 자동으로 새로고침할지 결정합니다.",
|
||||
"settings.weather.use_current_location": "현재 위치 사용",
|
||||
"settings.weather.location_unsupported": "현재 플랫폼에서 현재 위치 가져오기를 지원하지 않습니다.",
|
||||
"settings.weather.location_ready": "현재 Windows 위치를 사용할 수 있습니다.",
|
||||
"settings.weather.location_refreshing": "현재 위치 가져오는 중...",
|
||||
"settings.weather.location_refresh_success_format": "현재 위치 적용됨: {0}",
|
||||
"settings.weather.location_refresh_failed_format": "현재 위치 가져오기 실패: {0}",
|
||||
"settings.weather.preview_header": "연결 테스트",
|
||||
"settings.weather.preview_desc": "테스트 요청을 보내 현재 구성이 사용 가능한지 확인합니다.",
|
||||
"settings.weather.preview_button": "테스트 가져오기",
|
||||
"settings.weather.preview_section": "날씨 미리보기",
|
||||
"settings.weather.settings_section": "설정",
|
||||
"settings.weather.preview_panel_header": "날씨 미리보기",
|
||||
"settings.weather.preview_panel_desc": "현재 날씨 서비스 상태를 새로고침하고 확인합니다.",
|
||||
"settings.weather.refresh_button": "새로고침",
|
||||
"settings.weather.preview_updated_format": "{0}에 업데이트됨",
|
||||
"settings.weather.preview_hint": "테스트 가져오기를 통해 날씨 구성을 빠르게 확인할 수 있습니다.",
|
||||
"settings.weather.preview_missing_location": "테스트 전에 먼저 날씨 위치를 적용하세요.",
|
||||
"settings.weather.preview_success_format": "테스트 성공: {0} · {1} · {2}",
|
||||
"settings.weather.preview_failed_format": "테스트 실패: {0}",
|
||||
"settings.weather.preview_unknown": "알 수 없음",
|
||||
"settings.weather.alert_filter_header": "제외된 날씨 경보",
|
||||
"settings.weather.alert_filter_desc": "다음 키워드가 포함된 경보는 표시되지 않습니다. 한 줄에 하나의 규칙.",
|
||||
"settings.weather.alert_filter_placeholder": "한 줄에 하나의 키워드 입력",
|
||||
"settings.weather.icon_style_header": "날씨 아이콘 스타일",
|
||||
"settings.weather.icon_style_desc": "날씨 기호에 사용할 Fluent Icon 스타일을 선택합니다.",
|
||||
"settings.weather.icon_style_fluent_regular": "Fluent 윤곽선",
|
||||
"settings.weather.icon_style_fluent_filled": "Fluent 채우기",
|
||||
"settings.weather.no_tls_header": "TLS 없이 날씨 가져오기",
|
||||
"settings.weather.no_tls_desc": "권장하지 않음, 네트워크 호환성이 낮을 때만 시도하세요.",
|
||||
"settings.weather.status_city_empty": "도시 위치가 아직 구성되지 않았습니다.",
|
||||
"settings.weather.status_city_format": "모드: {0}|{1}|키: {2}",
|
||||
"settings.weather.status_coordinates_format": "모드: {0}|위도 {1:F4}, 경도 {2:F4}|키: {3}",
|
||||
"settings.weather.city_selection_label": "도시 선택",
|
||||
"settings.weather.coordinates_selection_label": "좌표 위치",
|
||||
"settings.weather.location_city_summary_desc": "날씨 조회에 사용할 현재 도시를 선택합니다.",
|
||||
"settings.weather.location_coordinates_summary_desc": "날씨 조회에 사용할 위도와 경도 및 선택적 위치 이름을 설정합니다.",
|
||||
"settings.weather.location_not_selected": "위치가 선택되지 않음",
|
||||
"settings.weather.alert_list_label": "제외 목록",
|
||||
"settings.weather.alert_list_desc": "한 줄에 하나의 제외 항목.",
|
||||
"settings.weather.no_tls_toggle": "호환성이 낮은 네트워크 환경에서 비 TLS 요청으로 대체 허용",
|
||||
"settings.weather.footer_hint": "바탕화면의 날씨 컴포넌트는 여기서 구성한 날씨 위치와 경보 제외 규칙을 공유합니다.",
|
||||
"settings.weather.location_header": "날씨 위치",
|
||||
"settings.weather.location_desc": "날씨 컴포넌트에 사용할 위치를 설정합니다.",
|
||||
"settings.weather.location_placeholder": "예: 서울",
|
||||
"settings.weather.location_apply": "저장",
|
||||
"settings.weather.location_empty": "날씨 위치가 아직 설정되지 않았습니다.",
|
||||
"settings.weather.location_required": "날씨 위치는 비워둘 수 없습니다.",
|
||||
"settings.weather.location_current_format": "현재 날씨 위치: {0}",
|
||||
"settings.weather.location_saved_format": "날씨 위치 저장됨: {0}",
|
||||
"weather.widget.location_not_configured": "날씨 위치가 구성되지 않음",
|
||||
"weather.widget.configure_hint": "설정 > 날씨에서 구성을 완료하세요",
|
||||
"weather.widget.loading": "로딩 중...",
|
||||
"weather.widget.fetch_failed": "날씨 가져오기 실패",
|
||||
"weather.widget.retrying": "나중에 자동으로 재시도",
|
||||
"weather.widget.location_unknown": "알 수 없는 위치",
|
||||
"weather.widget.condition_clear": "맑음",
|
||||
"weather.widget.condition_cloudy": "흐림",
|
||||
"weather.widget.condition_rain": "비",
|
||||
"weather.widget.condition_storm": "뇌우",
|
||||
"weather.widget.condition_snow": "눈",
|
||||
"weather.widget.condition_fog": "안개",
|
||||
"weather.widget.condition_unknown": "알 수 없는 날씨",
|
||||
"weather.widget.range_unknown": "-- / --",
|
||||
"weather.widget.range_format": "{0} / {1}",
|
||||
"schedule.widget.no_source": "ClassIsland 시간표를 읽지 못함",
|
||||
"schedule.widget.no_class_today": "오늘 수업 없음",
|
||||
"schedule.widget.layout_missing": "시간표 레이아웃 누락",
|
||||
"schedule.widget.subject_fallback": "이름 없는 수업",
|
||||
"schedule.widget.detail_fallback": "상세 정보 없음",
|
||||
"schedule.settings.title": "시간표 가져오기",
|
||||
"schedule.settings.desc": "ClassIsland CSES 시간표 파일을 가져오고 활성화 항목을 선택합니다.",
|
||||
"schedule.settings.add": "시간표 추가",
|
||||
"schedule.settings.empty": "가져온 시간표 없음",
|
||||
"schedule.settings.unnamed": "이름 없는 시간표",
|
||||
"schedule.settings.delete": "삭제",
|
||||
"schedule.settings.picker_title": "ClassIsland 시간표 파일 선택",
|
||||
"schedule.settings.picker_file_type.all": "ClassIsland 시간표 파일",
|
||||
"schedule.settings.picker_file_type.json": "ClassIsland 아카이브 (JSON)",
|
||||
"schedule.settings.picker_file_type.cses": "CSES 시간표 (YAML)",
|
||||
"schedule.settings.semester.title": "학기 설정",
|
||||
"schedule.settings.semester.start_date": "학기 시작일",
|
||||
"schedule.settings.semester.week_cycle": "주 순환",
|
||||
"schedule.settings.semester.week_cycle_desc": "다주 시간표 순환 주기를 설정하여 현재 몇 주차인지 계산합니다.",
|
||||
"schedule.settings.semester.week_cycle_format": "{0}주 순환",
|
||||
"worldclock.settings.title": "세계 시계 설정",
|
||||
"worldclock.settings.desc": "네 개의 시계에 대해 각각 시간대를 선택합니다.",
|
||||
"worldclock.settings.clock_1": "시계 1",
|
||||
"worldclock.settings.clock_2": "시계 2",
|
||||
"worldclock.settings.clock_3": "시계 3",
|
||||
"worldclock.settings.clock_4": "시계 4",
|
||||
"worldclock.settings.second_mode_label": "초침 방식",
|
||||
"worldclock.widget.today": "오늘",
|
||||
"worldclock.widget.yesterday": "어제",
|
||||
"worldclock.widget.tomorrow": "내일",
|
||||
"worldclock.widget.offset_same": "0시간",
|
||||
"worldclock.widget.offset_ahead_hours": "{0}시간 빠름",
|
||||
"worldclock.widget.offset_behind_hours": "{0}시간 늦음",
|
||||
"worldclock.widget.offset_ahead_hm": "{0}시간 {1}분 빠름",
|
||||
"worldclock.widget.offset_behind_hm": "{0}시간 {1}분 늦음",
|
||||
"weather.widget.aqi_unknown": "AQI --",
|
||||
"weather.widget.aqi_format": "AQI {0}",
|
||||
"weather.widget.updated_format": "{0:HH:mm}에 업데이트됨",
|
||||
"weather.hourly.now": "현재",
|
||||
"weather.hourly.sunset": "일몰",
|
||||
"weather.multiday.today": "오늘",
|
||||
"weather.multiday.tomorrow": "내일",
|
||||
"weather.multiday.aqi_format": "공기 좋음 {0}",
|
||||
"weather.multiday.aqi_unknown": "공기 --",
|
||||
"settings.region.title": "지역",
|
||||
"settings.region.description": "언어를 선택하고 설정 및 주요 인터페이스에 즉시 적용합니다.",
|
||||
"settings.region.language_header": "언어",
|
||||
"settings.region.language_label": "언어",
|
||||
"settings.region.language_zh": "중국어",
|
||||
"settings.region.language_en": "영어",
|
||||
"settings.region.language_ja": "일본어",
|
||||
"settings.region.language_ko": "한국어",
|
||||
"settings.region.timezone_header": "시간대",
|
||||
"settings.region.timezone_desc": "시간대를 선택합니다. 시계와 달력 컴포넌트가 이 시간대를 사용합니다.",
|
||||
"settings.region.applied_format": "언어가 {0}(으)로 전환되었습니다",
|
||||
"settings.region.follow_system": "시스템 기본값 따르기",
|
||||
"settings.general.title": "일반 설정",
|
||||
"settings.general.description": "언어, 시간대 및 런타임 동작을 조정합니다.",
|
||||
"settings.general.basic_header": "기본 설정",
|
||||
"settings.general.runtime_header": "런타임 설정",
|
||||
"settings.general.preview_header": "날짜 및 시간 미리보기",
|
||||
"settings.general.preview_time_label": "시간",
|
||||
"settings.general.preview_date_label": "날짜",
|
||||
"settings.general.render_mode_restart_message": "렌더링 모드 변경은 앱 재시작이 필요합니다.",
|
||||
"settings.appearance.title": "외관",
|
||||
"settings.appearance.description": "테마 소스, 시스템 소재 및 창 외관을 조정합니다.",
|
||||
"settings.appearance.theme_header": "테마",
|
||||
"settings.color.enable_night_mode_toggle": "야간 모드 활성화",
|
||||
"settings.color.use_system_chrome_toggle": "시스템 창 제목 표시줄 사용",
|
||||
"settings.color.theme_color_label": "테마 강조 색상",
|
||||
"settings.appearance.theme_color_mode_label": "테마 색상 소스",
|
||||
"settings.appearance.theme_color_mode.neutral": "기본 중성",
|
||||
"settings.appearance.theme_color_mode.user": "사용자 테마 색상 Monet",
|
||||
"settings.appearance.theme_color_mode.wallpaper": "배경화면 Monet 색상",
|
||||
"settings.appearance.theme_color_mode_desc.neutral": "표준 주간 흰색 배경 검은 텍스트와 야간 검은 배경 흰색 텍스트 중성색 표면을 사용합니다.",
|
||||
"settings.appearance.theme_color_mode_desc.user": "사용자가 선택한 테마 색상을 전체 바탕화면 셸의 Monet 시드 색상으로 사용합니다.",
|
||||
"settings.appearance.theme_color_mode_desc.wallpaper": "배경화면 색상을 사용합니다. 앱 배경화면을 우선하고 실패 시 시스템 바탕화면 배경화면으로 대체합니다.",
|
||||
"settings.appearance.theme_color_preview.app": "현재 앱 배경화면에서 추출한 색상을 미리보고 있습니다.",
|
||||
"settings.appearance.theme_color_preview.system": "현재 시스템 배경화면에서 추출한 색상을 미리보고 있습니다.",
|
||||
"settings.appearance.theme_color_preview.fallback": "사용 가능한 배경화면이 없어 현재 대체 강조 색상을 사용합니다.",
|
||||
"component.color_scheme.follow_system": "시스템 색상 구성 따르기",
|
||||
"component.color_scheme.native": "컴포넌트 사용자 지정 색상 구성 사용",
|
||||
"settings.appearance.system_material.none": "없음",
|
||||
"settings.appearance.system_material.mica": "Mica",
|
||||
"settings.appearance.system_material.acrylic": "Acrylic",
|
||||
"settings.appearance.system_material_desc.switchable": "선택한 소재를 창, Dock, 상태 표시줄 및 컴포넌트 호스트 배경에 적용합니다.",
|
||||
"settings.appearance.system_material_desc.fixed": "현재 시스템은 여기에 나열된 소재 모드만 제공합니다.",
|
||||
"settings.appearance.restart_message": "테마 색상 소스 및 시스템 소재 변경은 앱 재시작이 필요합니다.",
|
||||
"settings.appearance.preview.primary": "주 색상",
|
||||
"settings.appearance.preview.secondary": "보조 색상",
|
||||
"settings.appearance.preview.tertiary": "제3 색상",
|
||||
"settings.appearance.preview.neutral": "중성 색상",
|
||||
"settings.appearance.preview.seed": "시드 색상",
|
||||
"settings.appearance.preview.neutral_light": "흰색",
|
||||
"settings.appearance.preview.neutral_dark": "검은색",
|
||||
"settings.appearance.preview.apply_seed": "적용",
|
||||
"settings.appearance.preview.wallpaper_candidates": "배경화면 후보 테마 색상",
|
||||
"settings.appearance.preview.wallpaper_current": "현재",
|
||||
"settings.wallpaper.placement.fill": "채우기",
|
||||
"settings.wallpaper.placement.fit": "맞추기",
|
||||
"settings.wallpaper.placement.stretch": "늘리기",
|
||||
"settings.wallpaper.placement.center": "가운데",
|
||||
"settings.wallpaper.placement.tile": "바둑판",
|
||||
"settings.status_bar.clock_format_label": "시계 형식",
|
||||
"settings.status_bar.clock_format.hm": "시:분",
|
||||
"settings.status_bar.clock_format.hms": "시:분:초",
|
||||
"settings.components.title": "컴포넌트",
|
||||
"settings.components.description": "컴포넌트 레이아웃과 모서리 디자인을 조정합니다.",
|
||||
"settings.components.grid_header": "그리드 설정",
|
||||
"settings.components.header": "그리드 설정",
|
||||
"settings.components.short_side_label": "짧은 쪽 셀 수",
|
||||
"settings.components.edge_inset_label": "화면 여백",
|
||||
"settings.components.spacing_label": "컴포넌트 간격",
|
||||
"settings.components.spacing_compact": "컴팩트",
|
||||
"settings.components.spacing_relaxed": "여유 있음",
|
||||
"settings.components.corner_radius.header": "모서리 디자인",
|
||||
"settings.components.corner_radius.label": "컴포넌트 모서리",
|
||||
"settings.components.corner_radius.description": "컴포넌트 컨테이너 모서리를 직각에서 캡슐 모양에 가깝게 연속 조정하고 모서리가 커짐에 따라 내부 안전 영역도 확장합니다.",
|
||||
"settings.update.title": "업데이트",
|
||||
"settings.update.current_version_label": "현재 버전",
|
||||
"settings.update.latest_version_label": "최신 릴리스",
|
||||
"settings.update.published_at_label": "게시일",
|
||||
"settings.update.options_header": "업데이트 옵션",
|
||||
"settings.update.options_desc": "업데이트 확인과 릴리스 채널을 구성합니다.",
|
||||
"settings.update.auto_check_toggle": "시작 시 자동 업데이트 확인",
|
||||
"settings.update.include_prerelease_toggle": "사전 릴리스 버전 포함",
|
||||
"settings.update.channel_label": "업데이트 채널",
|
||||
"settings.update.channel_stable": "정식 버전",
|
||||
"settings.update.channel_preview": "미리보기 버전",
|
||||
"settings.update.actions_header": "업데이트 작업",
|
||||
"settings.update.actions_desc": "릴리스 확인, 설치 패키지 다운로드 및 업데이트 시작.",
|
||||
"settings.update.check_button": "업데이트 확인",
|
||||
"settings.update.download_install_button": "다운로드 및 설치",
|
||||
"settings.update.download_progress_idle": "다운로드 진행률: -",
|
||||
"settings.update.download_progress_format": "다운로드 진행률: {0:F0}%",
|
||||
"settings.update.status_ready": "업데이트 확인을 시작할 수 있습니다.",
|
||||
"settings.update.status_channel_changed": "업데이트 채널이 변경되었습니다. 다시 업데이트를 확인하세요.",
|
||||
"settings.update.status_channel_changed_format": "업데이트 채널이 {0}(으)로 전환되었습니다. 다시 업데이트를 확인하세요.",
|
||||
"settings.update.status_windows_only": "자동 설치 패키지 업데이트는 현재 Windows만 지원합니다.",
|
||||
"settings.update.status_checking": "GitHub Release 확인 중...",
|
||||
"settings.update.status_check_failed_format": "업데이트 확인 실패: {0}",
|
||||
"settings.update.status_up_to_date": "현재 최신 버전입니다.",
|
||||
"settings.update.status_asset_missing": "새 버전이 발견되었지만 호환되는 설치 패키지를 찾을 수 없습니다.",
|
||||
"settings.update.status_available_format": "새 버전 {0}이(가) 발견되었습니다. \"다운로드 및 설치\"를 클릭하여 계속하세요.",
|
||||
"settings.update.status_downloading": "설치 패키지 다운로드 중...",
|
||||
"settings.update.status_download_failed_format": "다운로드 실패: {0}",
|
||||
"settings.update.status_launching_installer": "다운로드 완료, 설치 프로그램 시작 중...",
|
||||
"settings.update.status_installer_missing": "다운로드 후 설치 패키지 파일을 찾을 수 없습니다.",
|
||||
"settings.update.status_installer_started": "설치 프로그램이 시작되었습니다. 앱이 업데이트를 위해 종료됩니다.",
|
||||
"settings.update.status_elevation_cancelled": "관리자 권한이 부여되지 않아 업데이트가 취소되었습니다.",
|
||||
"settings.update.status_launch_failed_format": "설치 프로그램 시작 실패: {0}",
|
||||
"settings.about.title": "정보",
|
||||
"settings.about.version_format": "버전: {0}",
|
||||
"settings.about.codename_format": "버전 코드명: {0}",
|
||||
"settings.about.font_format": "글꼴: {0}",
|
||||
"settings.about.startup_header": "Windows 자동 시작",
|
||||
"settings.about.startup_desc": "Windows 로그인 시 앱을 자동으로 시작합니다.",
|
||||
"settings.about.startup_toggle": "Windows 로그인 시 시작",
|
||||
"settings.about.render_mode_header": "앱 렌더링 모드",
|
||||
"settings.about.render_mode_desc": "앱 렌더링 백엔드를 선택합니다. 변경 후 앱 재시작이 필요합니다. 지원하지 않는 모드는 소프트웨어 렌더링으로 대체됩니다.",
|
||||
"settings.about.render_mode.default": "기본",
|
||||
"settings.about.render_mode.software": "소프트웨어",
|
||||
"settings.about.render_mode.angle_egl": "angleEgl",
|
||||
"settings.about.render_mode.wgl": "WGL",
|
||||
"settings.about.render_mode.vulkan": "Vulkan",
|
||||
"settings.about.render_mode.unknown": "알 수 없음",
|
||||
"settings.about.render_mode.current_label": "현재 실제 렌더링 백엔드",
|
||||
"settings.about.render_mode.current_format": "현재 백엔드: {0}",
|
||||
"settings.about.render_mode.impl_format": "런타임 구현: {0}",
|
||||
"settings.about.render_mode.impl_unavailable": "현재 런타임 구현 정보를 가져올 수 없습니다.",
|
||||
"settings.about.description": "앱 정보.",
|
||||
"settings.update.description": "업데이트 확인, 릴리스 채널 및 다운로드 소스 선택, 업데이트 설치 방법 제어.",
|
||||
"settings.update.status_card_title": "업데이트 상태",
|
||||
"settings.update.status_card_description": "새 버전 확인, 릴리스 정보 보기, 업데이트 시 다운로드 또는 설치 계속.",
|
||||
"settings.update.preferences_header": "업데이트 설정",
|
||||
"settings.update.preferences_description": "릴리스 채널, 설치 패키지 다운로드 소스, 설치 방법 및 다운로드 병렬 스레드 수를 선택합니다.",
|
||||
"settings.update.last_checked_label": "마지막 확인",
|
||||
"settings.update.source_label": "다운로드 소스",
|
||||
"settings.update.source_github": "GitHub",
|
||||
"settings.update.source_ghproxy": "gh-proxy",
|
||||
"settings.update.source_github_desc": "GitHub에서 직접 릴리스 설치 패키지를 다운로드합니다.",
|
||||
"settings.update.source_ghproxy_desc": "GitHub 릴리스 설치 패키지를 다운로드할 때 gh-proxy 미러를 사용합니다.",
|
||||
"settings.update.mode_label": "업데이트 모드",
|
||||
"settings.update.mode_manual": "수동 업데이트",
|
||||
"settings.update.mode_download_then_confirm": "자동 다운로드",
|
||||
"settings.update.mode_silent_on_exit": "자동 설치",
|
||||
"settings.update.mode_manual_desc": "업데이트만 확인합니다. 다운로드와 설치 시기는 사용자가 결정합니다.",
|
||||
"settings.update.mode_download_then_confirm_desc": "백그라운드에서 업데이트를 다운로드하고 완료 후 설치 여부를 확인합니다.",
|
||||
"settings.update.mode_silent_on_exit_desc": "백그라운드에서 업데이트를 다운로드하고 다음 앱 종료 시 자동으로 설치합니다.",
|
||||
"settings.update.channel_stable_desc": "정식 버전은 안정성을 우선하며 대부분의 사용자에게 적합합니다.",
|
||||
"settings.update.channel_preview_desc": "미리보기 버전은 더 빠른 새 기능을 포함할 수 있지만 안정성이 낮을 수 있습니다.",
|
||||
"settings.update.download_threads_label": "다운로드 스레드 수",
|
||||
"settings.update.download_threads_desc": "앱 업데이트 설치 패키지에 사용할 병렬 다운로드 스레드 수를 설정합니다.",
|
||||
"settings.update.force_check_label": "강제 업데이트 확인",
|
||||
"settings.update.force_check_desc": "버전 비교를 무시하고 GitHub에서 강제로 최신 버전을 가져옵니다.",
|
||||
"settings.update.status_force_checking": "GitHub 릴리스 강제 확인 중...",
|
||||
"settings.update.status_force_no_asset": "릴리스를 찾았지만 호환되는 설치 프로그램이 없습니다.",
|
||||
"settings.update.status_force_available_format": "릴리스 {0}을(를) 사용할 수 있습니다. '다운로드 및 설치'를 클릭하세요.",
|
||||
"settings.update.install_now_button": "지금 설치",
|
||||
"settings.update.status_downloaded_confirm": "업데이트가 다운로드되었습니다. 확인 후 설치 시기를 선택하세요.",
|
||||
"settings.update.status_downloaded_exit": "업데이트가 다운로드되었습니다. 앱 종료 시 설치됩니다.",
|
||||
"settings.about.app_info_header": "앱 정보",
|
||||
"settings.about.update_header": "업데이트",
|
||||
"settings.about.version_label": "버전",
|
||||
"settings.about.codename_label": "버전 코드명",
|
||||
"settings.about.render_backend_label": "렌더링 백엔드",
|
||||
"settings.about.render_backend_format": "렌더링 백엔드: {0}",
|
||||
"settings.restart_dialog.title": "앱 재시작 필요",
|
||||
"settings.restart_dialog.render_mode_message": "렌더링 모드를 \"{0}\"에서 \"{1}\"(으)로 변경하려면 앱을 재시작해야 합니다. 지금 재시작하시겠습니까?",
|
||||
"settings.restart_dialog.restart": "지금 재시작",
|
||||
"settings.restart_dialog.later": "나중에",
|
||||
"settings.restart_dialog.cancel": "취소",
|
||||
"settings.restart_dock.title": "앱 재시작 필요",
|
||||
"settings.restart_dock.description": "일부 변경 사항은 앱 재시작 후에 적용됩니다.",
|
||||
"settings.restart_dock.button": "앱 재시작",
|
||||
"settings.footer": "LanMountainDesktop 설정",
|
||||
"filepicker.title": "배경화면 선택",
|
||||
"filepicker.image_files": "이미지 파일",
|
||||
"common.day": "주간",
|
||||
"common.night": "야간",
|
||||
"common.back": "뒤로",
|
||||
"common.close": "닫기",
|
||||
"common.unknown": "알 수 없는 오류",
|
||||
"common.recommended": "추천",
|
||||
"common.monet": "Monet",
|
||||
"desktop.page_index_format": "바탕화면 {0}",
|
||||
"launcher.title": "앱 런처",
|
||||
"launcher.folder": "폴더",
|
||||
"launcher.subtitle": "Windows 시작 메뉴 구조에 따라 모든 앱과 폴더 표시",
|
||||
"launcher.subtitle_linux": "Linux .desktop 항목에서 스캔한 설치된 앱 표시",
|
||||
"launcher.empty": "시작 메뉴 항목을 찾을 수 없습니다.",
|
||||
"launcher.empty_linux": "Linux .desktop 앱 항목을 찾을 수 없습니다.",
|
||||
"launcher.empty_folder": "이 폴더는 비어 있습니다.",
|
||||
"launcher.folder_items_format": "{0}개 앱",
|
||||
"launcher.context.hide_icon": "아이콘 숨기기",
|
||||
"launcher.action.hide": "숨기기",
|
||||
"settings.launcher.title": "앱 런처",
|
||||
"settings.launcher.description": "앱 런처에서 숨겨진 앱과 폴더를 관리합니다.",
|
||||
"settings.launcher.hidden_header": "숨겨진 항목",
|
||||
"settings.launcher.hidden_desc": "숨겨진 런처 항목을 보고 다시 표시합니다.",
|
||||
"settings.launcher.hidden_hint": "바탕화면 편집 모드에서 런처 아이콘을 선택하고 \"숨기기\"를 클릭하면 숨겨진 항목이 여기에 표시됩니다.",
|
||||
"settings.launcher.hidden_empty": "숨겨진 항목이 없습니다.",
|
||||
"settings.launcher.hidden_summary_format": "총 {0}개 숨겨진 항목",
|
||||
"settings.launcher.hidden_type_folder": "폴더",
|
||||
"settings.launcher.hidden_type_shortcut": "앱",
|
||||
"settings.launcher.restore_button": "숨기기 해제",
|
||||
"settings.plugins.title": "플러그인",
|
||||
"settings.plugins.runtime_header": "플러그인 런타임",
|
||||
"settings.plugins.runtime_desc": "플러그인 런타임 상태, 로드 결과 및 진단 정보를 확인합니다.",
|
||||
"settings.plugins.runtime_hint": "설치된 플러그인의 발견 결과, 로드 상태 및 런타임 진단 정보가 여기에 표시됩니다.",
|
||||
"settings.plugins.runtime_status": "플러그인 스캔이 완료되면 런타임 상태가 여기에 표시됩니다.",
|
||||
"settings.plugins.description": "설치된 플러그인을 관리하고 런타임 상태를 확인합니다.",
|
||||
"settings.plugins.initial_status": "플러그인 상태를 새로고침하여 최신 설치된 플러그인을 확인하세요.",
|
||||
"settings.plugins.refresh_button": "플러그인 새로고침",
|
||||
"settings.plugins.refresh_success_installed_format": "{0}개 설치된 플러그인을 로드했습니다.",
|
||||
"settings.plugins.refresh_success_format": "{0}개 설치된 플러그인과 {1}개 마켓 항목을 로드했습니다.",
|
||||
"settings.plugins.refresh_failed": "플러그인 카탈로그 인덱스 로드 실패.",
|
||||
"settings.plugins.marketplace_header": "플러그인 카탈로그",
|
||||
"settings.plugins.marketplace_empty": "현재 사용 가능한 마켓 플러그인이 없습니다.",
|
||||
"settings.plugins.delete_button_short": "삭제",
|
||||
"settings.plugins.install_button_short": "설치",
|
||||
"settings.plugins.restart_required": "플러그인 변경 사항은 재시작 후 적용됩니다.",
|
||||
"settings.plugins.toggle_unchanged_format": "플러그인 \"{0}\"에 변경 사항이 없습니다.",
|
||||
"settings.plugins.delete_failed_name_format": "플러그인 \"{0}\" 제거 실패.",
|
||||
"settings.plugins.install_failed_name_format": "플러그인 \"{0}\" 설치 실패.",
|
||||
"settings.plugins.installed_header": "설치된 플러그인",
|
||||
"settings.plugins.installed_desc": "여기서 설치된 플러그인을 보고 삭제합니다.",
|
||||
"settings.plugins.import_header": "설치 패키지에서 가져오기",
|
||||
"settings.plugins.import_desc": ".laapp 플러그인 패키지를 열고 로컬 플러그인 디렉토리에 스테이징합니다.",
|
||||
"settings.plugins.restart_hint": "플러그인 설치 및 삭제 변경 사항은 앱 재시작 후 적용됩니다.",
|
||||
"settings.plugins.empty": "플러그인을 찾을 수 없습니다.",
|
||||
"settings.plugins.runtime_unavailable": "플러그인 런타임을 사용할 수 없습니다.",
|
||||
"settings.plugins.summary_format": "총 {0}개 플러그인 감지됨; {1}개 활성화됨; {2}개 로드됨; {3}개 설정 페이지; {4}개 컴포넌트; {5}개 실패.",
|
||||
"settings.plugins.summary_item_format": "{0} v{1} | {2}",
|
||||
"settings.plugins.state.enabled": "활성화됨",
|
||||
"settings.plugins.state.enabled_failed": "활성화됨 / 로드 실패",
|
||||
"settings.plugins.state.disabled": "비활성화됨",
|
||||
"settings.plugins.state.loaded": "로드됨",
|
||||
"settings.plugins.state.load_failed": "로드 실패",
|
||||
"settings.plugins.toggle_on": "활성화",
|
||||
"settings.plugins.toggle_off": "비활성화",
|
||||
"settings.plugins.toggle_result_format": "플러그인 \"{0}\"이(가) 다음 시작 시 {1}(으)로 설정되었습니다. 앱 재시작 후 설정 페이지와 컴포넌트 변경 사항이 적용됩니다.",
|
||||
"settings.plugins.toggle_state_enabled": "활성화",
|
||||
"settings.plugins.toggle_state_disabled": "비활성화",
|
||||
"settings.plugins.toggle_failed_detail_format": "플러그인 \"{0}\" 상태 업데이트 실패: {1}",
|
||||
"settings.plugins.install_button": ".laapp 플러그인 패키지 열기",
|
||||
"settings.plugins.install_unavailable": "플러그인 런타임을 사용할 수 없어 일시적으로 .laapp 플러그인 패키지를 설치할 수 없습니다.",
|
||||
"settings.plugins.install_hint_format": ".laapp 플러그인 패키지를 열어 설치: {0}",
|
||||
"settings.plugins.install_picker_title": "플러그인 설치 패키지 선택",
|
||||
"settings.plugins.install_file_type": ".laapp 플러그인 패키지",
|
||||
"settings.plugins.install_picker_unavailable": "파일 저장소 제공자를 사용할 수 없습니다.",
|
||||
"settings.plugins.install_copy_failed": "선택한 .laapp 플러그인 패키지 복사 실패.",
|
||||
"settings.plugins.install_success_format": "플러그인 \"{0}\" 설치 완료. 앱 재시작 후 새 설정 페이지와 컴포넌트가 적용됩니다.",
|
||||
"settings.plugins.install_failed_format": "플러그인 패키지 설치 실패: {0}",
|
||||
"settings.plugins.delete_button": "플러그인 삭제",
|
||||
"settings.plugins.delete_success_format": "플러그인 \"{0}\"이(가) 삭제 예정입니다. 앱 재시작 후 제거가 완료됩니다.",
|
||||
"settings.plugins.delete_failed_format": "플러그인 삭제 실패: {0}",
|
||||
"settings.plugins.delete_failed_detail_format": "플러그인 \"{0}\" 삭제 실패: {1}",
|
||||
"settings.plugins.publisher_format": "게시자: {0}",
|
||||
"settings.plugins.publisher_unknown": "알 수 없는 게시자",
|
||||
"settings.plugins.source_package": ".laapp 패키지",
|
||||
"settings.plugins.source_manifest": "매니페스트 파일",
|
||||
"settings.plugins.subtitle_format": "{0} | {1} | {2}",
|
||||
"settings.plugins.detail_format": "설정 페이지: {0} | 컴포넌트: {1}",
|
||||
"settings.nav.plugin_catalog": "플러그인 카탈로그",
|
||||
"settings.plugin_catalog.title": "플러그인 카탈로그",
|
||||
"settings.plugin_catalog.subtitle": "LanAirApp 공식 소스의 플러그인을 탐색하고 로컬에 설치 스테이징합니다.",
|
||||
"settings.plugin_catalog.unavailable": "플러그인 런타임을 사용할 수 없어 일시적으로 공식 카탈로그를 열 수 없습니다.",
|
||||
"settings.update.status_idle": "아직 업데이트 확인이 수행되지 않았습니다.",
|
||||
"settings.update.status_preferences_saved": "업데이트 설정이 저장되었습니다.",
|
||||
"settings.update.status_check_failed": "업데이트 확인 실패.",
|
||||
"settings.update.status_available_summary_format": "업데이트 발견: {0} (현재: {1}).",
|
||||
"settings.update.status_up_to_date_format": "현재 최신 버전입니다 ({0}).",
|
||||
"settings.window.drawer_default": "상세 정보",
|
||||
"market.toolbar.search_placeholder": "플러그인 검색",
|
||||
"market.toolbar.refresh": "새로고침",
|
||||
"market.status.loading": "공식 플러그인 카탈로그 로딩 중...",
|
||||
"market.status.loaded_network_format": "공식 소스에서 {0}개 플러그인을 로드했습니다.",
|
||||
"market.status.loaded_cache_format": "공식 소스를 일시적으로 사용할 수 없어 캐시에서 {0}개 플러그인을 로드했습니다. 원인: {1}",
|
||||
"market.status.load_failed_format": "플러그인 카탈로그 로드 실패: {0}",
|
||||
"market.status.installing_format": "플러그인 \"{0}\" 다운로드 및 스테이징 중...",
|
||||
"market.status.install_success_format": "플러그인 \"{0}\" 스테이징 완료. 앱 재시작 후 적용됩니다.",
|
||||
"market.status.install_failed_format": "플러그인 설치 실패: {0}",
|
||||
"market.status.host_incompatible_format": "현재 호스트 버전이 너무 낮습니다. 최소 {0}이(가) 필요합니다.",
|
||||
"market.list.empty": "플러그인 카탈로그이 아직 로드되지 않았습니다.",
|
||||
"market.list.no_results": "현재 검색과 일치하는 플러그인이 없습니다.",
|
||||
"market.card.subtitle_format": "{0} | v{1}",
|
||||
"market.card.loaded": "로드됨",
|
||||
"market.card.pending_restart": "재시작 필요",
|
||||
"market.detail.placeholder": "왼쪽에서 플러그인을 선택하여 상세 정보를 확인하세요.",
|
||||
"market.detail.author": "게시자",
|
||||
"market.detail.version": "버전",
|
||||
"market.detail.api_version": "API 버전",
|
||||
"market.detail.min_host_version": "최소 호스트 버전",
|
||||
"market.detail.installed_version": "설치된 버전",
|
||||
"market.detail.not_installed": "미설치",
|
||||
"market.detail.readme": "README",
|
||||
"market.detail.plugin_information": "플러그인 정보",
|
||||
"market.detail.author_subtitle_format": "작성자: {0}",
|
||||
"market.detail.package_size": "패키지 크기",
|
||||
"market.detail.published_at": "최초 게시",
|
||||
"market.detail.updated_at": "최근 업데이트",
|
||||
"market.detail.tags": "태그",
|
||||
"market.detail.project": "프로젝트",
|
||||
"market.detail.state": "설치 상태",
|
||||
"market.detail.market_source": "마켓 소스",
|
||||
"market.detail.homepage": "홈페이지",
|
||||
"market.detail.repository": "저장소",
|
||||
"market.detail.release_notes": "릴리스 노트",
|
||||
"market.detail.dependencies": "의존성",
|
||||
"market.detail.dependencies_empty": "이 플러그인은 SharedContracts 의존성을 선언하지 않았습니다.",
|
||||
"market.detail.readme_loading": "README 로딩 중...",
|
||||
"market.detail.readme_empty": "README가 비어 있습니다.",
|
||||
"market.detail.readme_error_format": "README 로드 실패: {0}",
|
||||
"market.detail.state.not_installed": "미설치",
|
||||
"market.detail.state.update_available": "업데이트 가능",
|
||||
"market.detail.state.installed": "설치됨",
|
||||
"market.detail.unknown": "알 수 없음",
|
||||
"market.button.install": "설치",
|
||||
"market.button.update": "업데이트",
|
||||
"market.button.installed": "설치됨",
|
||||
"market.button.installing": "설치 중...",
|
||||
"market.button.restart": "재시작 후 적용",
|
||||
"button.component_library": "바탕화면 편집",
|
||||
"tooltip.component_library": "바탕화면 편집",
|
||||
"component_library.title": "바탕화면 편집",
|
||||
"component_library.empty": "좌우로 스와이프하여 카테고리를 선택하고 클릭하여 진입한 후 컴포넌트를 바탕화면에 드래그하여 배치하세요.",
|
||||
"component_library.drag_hint": "드래그하여 배치",
|
||||
"component.delete": "삭제",
|
||||
"component.edit": "편집",
|
||||
"component.editor.instance_scope": "설정은 현재 컴포넌트 인스턴스에만 적용됩니다.",
|
||||
"component.editor.info_header": "컴포넌트 정보",
|
||||
"component.editor.id_label": "컴포넌트 ID",
|
||||
"component.editor.placement_label": "인스턴스 ID",
|
||||
"component.editor.scope_label": "범위",
|
||||
"component.editor.scope_instance": "인스턴스 수준 편집기",
|
||||
"component_category.clock": "시계",
|
||||
"component_category.date": "달력",
|
||||
"component_category.weather": "날씨",
|
||||
"component_category.board": "화이트보드",
|
||||
"component_category.media": "미디어",
|
||||
"component_category.info": "정보 추천",
|
||||
"component_category.calculator": "계산기",
|
||||
"component_category.study": "공부",
|
||||
"component_category.file": "파일",
|
||||
"component.date": "달력",
|
||||
"component.month_calendar": "월간 달력",
|
||||
"component.lunar_calendar": "음력",
|
||||
"component.desktop_clock": "시계",
|
||||
"component.weather_clock": "날씨 시계",
|
||||
"component.world_clock": "세계 시계",
|
||||
"component.desktop_timer": "타이머",
|
||||
"component.desktop_weather": "날씨",
|
||||
"component.hourly_weather": "시간별 날씨",
|
||||
"component.multiday_weather": "다일 날씨",
|
||||
"component.extended_weather": "확장 날씨",
|
||||
"component.class_schedule": "시간표",
|
||||
"component.music_control": "음악 제어",
|
||||
"component.audio_recorder": "녹음",
|
||||
"component.daily_poetry": "매일 시",
|
||||
"component.daily_artwork": "매일 명화",
|
||||
"component.daily_word": "매일 단어",
|
||||
"component.daily_word_2x2": "매일 단어 2x2",
|
||||
"component.cnr_daily_news": "CNR 뉴스",
|
||||
"component.ifeng_news": "Ifeng 뉴스",
|
||||
"component.bilibili_hot_search": "Bilibili 인기 검색",
|
||||
"component.baidu_hot_search": "Baidu 인기 검색",
|
||||
"component.stcn24_forum": "STCN 24",
|
||||
"component.exchange_rate_converter": "환율 변환기",
|
||||
"component.whiteboard": "세로 작은 칠판",
|
||||
"component.blackboard_landscape": "가로 작은 칠판",
|
||||
"component.browser": "브라우저",
|
||||
"component.office_recent_documents": "최근 문서",
|
||||
"whiteboard.settings.desc": "각 작은 칠판은 독립적으로 자신의 노트 기록을 저장합니다.",
|
||||
"whiteboard.settings.retention.title": "노트 보존 기간",
|
||||
"whiteboard.settings.retention.desc": "이 작은 칠판에서 만료된 노트가 자동 삭제되기 전에 저장된 노트를 얼마나 오래 보존할지 선택합니다.",
|
||||
"whiteboard.settings.retention.option": "{0}일",
|
||||
"whiteboard.settings.instance_scope": "이 보존 기간 설정은 각 작은 칠판 컴포넌트 인스턴스별로 개별 저장됩니다.",
|
||||
"office_recent_documents.settings.desc": "이 위젯이 스캔할 Windows 및 Office 최근 문서 소스를 선택합니다.",
|
||||
"office_recent_documents.settings.sources_title": "최근 문서 소스",
|
||||
"office_recent_documents.settings.sources_desc": "여러 소스를 동시에 선택할 수 있습니다. 레지스트리 소스를 선택하면 Office interop MRU 대체도 유지됩니다.",
|
||||
"office_recent_documents.settings.source.registry": "Office 레지스트리 MRU",
|
||||
"office_recent_documents.settings.source.recent_folders": "Windows 최근 폴더",
|
||||
"office_recent_documents.settings.source.jump_lists": "Windows 점프 목록",
|
||||
"office_recent_documents.settings.hint": "모든 소스를 끄면 최소 하나의 소스를 다시 활성화할 때까지 이 위젯은 비어 있게 됩니다.",
|
||||
"component.holiday_calendar": "공휴일 달력",
|
||||
"component.study_environment": "환경",
|
||||
"component.study_session_control": "공부 시간 제어",
|
||||
"component.study_session_history": "기록 시간 데이터",
|
||||
"component.study_noise_curve": "소음 곡선",
|
||||
"component.study_noise_distribution": "소음 레벨 분포",
|
||||
"component.study_score_overview": "공부 점수 개요",
|
||||
"component.study_deduction_reasons": "감점 원인",
|
||||
"component.study_interrupt_density": "방해 밀도",
|
||||
"desktop_clock.settings.title": "시계 설정",
|
||||
"desktop_clock.settings.desc": "단일 시계의 시간대를 선택합니다.",
|
||||
"desktop_clock.settings.timezone_label": "시간대",
|
||||
"desktop_clock.settings.second_mode_label": "초침 방식",
|
||||
"clock.second_mode.tick": "똑딱이",
|
||||
"clock.second_mode.sweep": "스윕",
|
||||
"poetry.widget.loading_content": "시 불러오는 중",
|
||||
"poetry.widget.loading_author": "로딩 중",
|
||||
"poetry.widget.fetch_failed": "시 가져오기 실패",
|
||||
"poetry.widget.fallback_content": "오늘의 시를 사용할 수 없습니다",
|
||||
"poetry.widget.fallback_author": "나중에 다시 시도하세요",
|
||||
"poetry.widget.unknown_author": "익명",
|
||||
"artwork.widget.loading": "로딩 중",
|
||||
"artwork.widget.loading_title": "매일 명화",
|
||||
"artwork.widget.loading_subtitle": "오늘의 명화 가져오는 중",
|
||||
"artwork.widget.fetch_failed": "명화 가져오기 실패",
|
||||
"artwork.widget.fallback_title": "매일 명화",
|
||||
"artwork.widget.fallback_artist": "추천 서비스를 사용할 수 없습니다",
|
||||
"artwork.widget.fallback_year": "나중에 다시 시도하세요",
|
||||
"artwork.widget.unknown_artist": "알 수 없는 작가",
|
||||
"dailyword.widget.loading": "로딩 중...",
|
||||
"dailyword.widget.loading_word": "매일 단어",
|
||||
"dailyword.widget.loading_pronunciation": "발음 가져오는 중",
|
||||
"dailyword.widget.loading_meaning": "뜻 가져오는 중",
|
||||
"dailyword.widget.loading_example": "예문 가져오는 중",
|
||||
"dailyword.widget.loading_example_translation": "로딩 중",
|
||||
"dailyword.widget.fetch_failed": "매일 단어 가져오기 실패",
|
||||
"dailyword.widget.fallback_word": "매일 단어",
|
||||
"dailyword.widget.fallback_pronunciation": "발음을 사용할 수 없습니다",
|
||||
"dailyword.widget.fallback_meaning": "Youdao 사전을 사용할 수 없습니다",
|
||||
"dailyword.widget.fallback_example": "오른쪽 상단 새로고침을 클릭하여 다시 시도하세요",
|
||||
"dailyword.widget.fallback_example_translation": "네트워크 복구 후 자동 업데이트됩니다",
|
||||
"dailyword2x2.widget.tap_to_show": "탭하여 뜻 보기",
|
||||
"cnrnews.widget.loading": "로딩 중...",
|
||||
"cnrnews.widget.loading_title": "뉴스 헤드라인 가져오는 중",
|
||||
"cnrnews.widget.loading_subtitle": "잠시 기다려주세요",
|
||||
"cnrnews.widget.fetch_failed": "뉴스 가져오기 실패",
|
||||
"cnrnews.widget.fallback_title": "CNR 뉴스를 사용할 수 없습니다",
|
||||
"cnrnews.widget.fallback_subtitle": "오른쪽 상단을 클릭하여 나중에 다시 시도하세요",
|
||||
"cnrnews.widget.hot_label": "핫",
|
||||
"bilihot.widget.brand": "bilibili 인기 검색",
|
||||
"bilihot.widget.top_right_label": "bilibili 인기 검색",
|
||||
"bilihot.widget.search_entry": "검색",
|
||||
"bilihot.widget.search_placeholder": "인기 검색어 검색",
|
||||
"bilihot.widget.loading": "로딩 중...",
|
||||
"bilihot.widget.loading_item": "로딩 중...",
|
||||
"bilihot.widget.fetch_failed": "인기 검색 가져오기 실패",
|
||||
"bilihot.widget.fallback_item": "인기 검색 없음",
|
||||
"bilihot.widget.more_hot": "더 많은 인기 검색",
|
||||
"baiduhot.widget.brand": "Baidu 인기 검색",
|
||||
"baiduhot.widget.loading": "로딩 중...",
|
||||
"baiduhot.widget.loading_item": "로딩 중...",
|
||||
"baiduhot.widget.fetch_failed": "인기 검색 가져오기 실패",
|
||||
"baiduhot.widget.fallback_item": "인기 검색 없음",
|
||||
"baiduhot.widget.refresh_tooltip": "새로고침",
|
||||
"ifeng.widget.brand": "Ifeng 뉴스",
|
||||
"ifeng.widget.loading": "로딩 중...",
|
||||
"ifeng.widget.loading_item": "로딩 중...",
|
||||
"ifeng.widget.fetch_failed": "뉴스 가져오기 실패",
|
||||
"ifeng.widget.fallback_item": "뉴스 없음",
|
||||
"ifeng.widget.refresh_tooltip": "새로고침",
|
||||
"dailyword.settings.title": "매일 단어 설정",
|
||||
"dailyword.settings.desc": "자동 새로고침 설정과 새로고침 빈도를 구성합니다.",
|
||||
"dailyword.settings.auto_refresh_label": "자동 새로고침",
|
||||
"dailyword.settings.auto_refresh_enabled": "자동 새로고침 활성화",
|
||||
"dailyword.settings.frequency_label": "새로고침 빈도",
|
||||
"bilihot.settings.title": "Bilibili 인기 검색 설정",
|
||||
"bilihot.settings.desc": "자동 새로고침 설정과 새로고침 빈도를 구성합니다.",
|
||||
"bilihot.settings.auto_refresh_label": "자동 새로고침",
|
||||
"bilihot.settings.auto_refresh_enabled": "자동 새로고침 활성화",
|
||||
"bilihot.settings.frequency_label": "새로고침 빈도",
|
||||
"baiduhot.settings.title": "Baidu 인기 검색 설정",
|
||||
"baiduhot.settings.desc": "데이터 소스, 자동 새로고침 설정과 새로고침 빈도를 구성합니다.",
|
||||
"baiduhot.settings.source_label": "데이터 소스",
|
||||
"baiduhot.settings.source_official": "Baidu 공식 소스",
|
||||
"baiduhot.settings.source_rss": "타사 RSS 소스",
|
||||
"baiduhot.settings.auto_refresh_label": "자동 새로고침",
|
||||
"baiduhot.settings.auto_refresh_enabled": "자동 새로고침 활성화",
|
||||
"baiduhot.settings.frequency_label": "새로고침 빈도",
|
||||
"ifeng.settings.title": "Ifeng 뉴스 설정",
|
||||
"ifeng.settings.desc": "채널, 자동 새로고침 설정과 새로고침 빈도를 구성합니다.",
|
||||
"ifeng.settings.channel_label": "뉴스 채널",
|
||||
"ifeng.settings.channel_comprehensive": "종합",
|
||||
"ifeng.settings.channel_mainland": "중국 본토",
|
||||
"ifeng.settings.channel_taiwan": "대만",
|
||||
"ifeng.settings.auto_refresh_label": "자동 새로고침",
|
||||
"ifeng.settings.auto_refresh_enabled": "자동 새로고침 활성화",
|
||||
"ifeng.settings.frequency_label": "새로고침 빈도",
|
||||
"refresh.frequency.5m": "5분",
|
||||
"refresh.frequency.10m": "10분",
|
||||
"refresh.frequency.12m": "12분",
|
||||
"refresh.frequency.15m": "15분",
|
||||
"refresh.frequency.20m": "20분",
|
||||
"refresh.frequency.30m": "30분",
|
||||
"refresh.frequency.40m": "40분",
|
||||
"refresh.frequency.1h": "1시간",
|
||||
"refresh.frequency.3h": "3시간",
|
||||
"refresh.frequency.6h": "6시간",
|
||||
"refresh.frequency.12h": "12시간",
|
||||
"refresh.frequency.24h": "24시간",
|
||||
"weather.widget.settings.title": "날씨 컴포넌트 설정",
|
||||
"weather.widget.settings.desc": "모든 날씨 컴포넌트의 자동 새로고침 설정과 새로고침 빈도를 구성합니다.",
|
||||
"weather.widget.settings.auto_refresh_label": "자동 새로고침",
|
||||
"weather.widget.settings.auto_refresh_enabled": "자동 새로고침 활성화",
|
||||
"weather.widget.settings.frequency_label": "새로고침 빈도",
|
||||
"weather.widget.settings.frequency_10m": "10분",
|
||||
"weather.widget.settings.frequency_12m": "12분",
|
||||
"weather.widget.settings.frequency_15m": "15분",
|
||||
"weather.widget.settings.frequency_30m": "30분",
|
||||
"weather.widget.settings.frequency_1h": "1시간",
|
||||
"weather.widget.settings.frequency_3h": "3시간",
|
||||
"stcn24.widget.loading": "로딩 중...",
|
||||
"stcn24.widget.loading_item": "로딩 중...",
|
||||
"stcn24.widget.fetch_failed": "게시물 가져오기 실패",
|
||||
"stcn24.widget.fallback_item": "게시물 없음",
|
||||
"stcn24.settings.title": "STCN 24 설정",
|
||||
"stcn24.settings.desc": "정보 소스, 자동 새로고침 설정과 새로고침 빈도를 구성합니다.",
|
||||
"stcn24.settings.source_label": "정보 소스",
|
||||
"stcn24.settings.source_latest_created": "최신 게시",
|
||||
"stcn24.settings.source_latest_activity": "최신 답변",
|
||||
"stcn24.settings.source_most_replies": "답변 많음",
|
||||
"stcn24.settings.source_earliest_created": "가장 오래된 게시",
|
||||
"stcn24.settings.source_earliest_activity": "가장 오래된 답변",
|
||||
"stcn24.settings.source_least_replies": "답변 적음",
|
||||
"stcn24.settings.source_frontpage_latest": "프론트 추천 (신규)",
|
||||
"stcn24.settings.source_frontpage_earliest": "프론트 추천 (구형)",
|
||||
"stcn24.settings.auto_refresh_label": "자동 새로고침",
|
||||
"stcn24.settings.auto_refresh_enabled": "자동 새로고침 활성화",
|
||||
"stcn24.settings.frequency_label": "새로고침 빈도",
|
||||
"stcn24.settings.frequency_5m": "5분",
|
||||
"stcn24.settings.frequency_10m": "10분",
|
||||
"stcn24.settings.frequency_20m": "20분",
|
||||
"stcn24.settings.frequency_30m": "30분",
|
||||
"stcn24.settings.frequency_1h": "1시간",
|
||||
"stcn24.settings.frequency_3h": "3시간",
|
||||
"exchange.widget.loading": "환율 로딩 중...",
|
||||
"exchange.widget.fetch_failed": "환율 가져오기 실패",
|
||||
"cnrnews.settings.title": "CNR 뉴스 설정",
|
||||
"cnrnews.settings.desc": "뉴스 자동 순환과 새로고침 빈도를 구성합니다.",
|
||||
"cnrnews.settings.auto_rotate_label": "자동 순환",
|
||||
"cnrnews.settings.auto_rotate_enabled": "자동 순환 활성화",
|
||||
"cnrnews.settings.frequency_label": "순환 빈도",
|
||||
"cnrnews.settings.frequency_5m": "5분",
|
||||
"cnrnews.settings.frequency_10m": "10분",
|
||||
"cnrnews.settings.frequency_40m": "40분",
|
||||
"cnrnews.settings.frequency_1h": "1시간",
|
||||
"cnrnews.settings.frequency_12h": "12시간",
|
||||
"cnrnews.settings.frequency_24h": "24시간",
|
||||
"artwork.settings.title": "매일 이미지 설정",
|
||||
"artwork.settings.desc": "매일 이미지의 데이터 소스를 전환합니다.",
|
||||
"artwork.settings.source_label": "미러 소스",
|
||||
"artwork.settings.source_domestic": "국내 미러",
|
||||
"artwork.settings.source_overseas": "해외 미러",
|
||||
"artwork.settings.source_status_domestic": "현재 소스: 국내 미러 (중국 네트워크 우선)",
|
||||
"artwork.settings.source_status_overseas": "현재 소스: 해외 미러 (미술관 추천)",
|
||||
"music.widget.unsupported": "현재 플랫폼에서 음악 제어를 지원하지 않습니다",
|
||||
"music.widget.unsupported_hint": "이 컴포넌트는 Windows SMTC만 지원합니다",
|
||||
"music.widget.no_session": "음원 없음",
|
||||
"music.widget.no_session_hint": "앱 스토어에서 \"QQ Music/Kugou Music/NetEase Cloud Music\"을 다운로드한 후 사용하세요",
|
||||
"music.widget.open_player": "플레이어 열기",
|
||||
"music.widget.unknown_title": "알 수 없는 곡",
|
||||
"music.widget.unknown_artist": "알 수 없는 아티스트",
|
||||
"music.widget.status.opened": "열림",
|
||||
"music.widget.status.changing": "전환 중",
|
||||
"music.widget.status.stopped": "정지됨",
|
||||
"music.widget.status.playing": "재생 중",
|
||||
"music.widget.status.paused": "일시정지됨",
|
||||
"recording.widget.title": "녹음",
|
||||
"recording.widget.hint.ready": "빨간 버튼을 클릭하여 시작",
|
||||
"recording.widget.hint.recording": "녹음 중",
|
||||
"recording.widget.hint.paused": "일시정지됨",
|
||||
"recording.widget.hint.unsupported": "마이크를 사용할 수 없음",
|
||||
"recording.widget.hint.error": "녹음 실패",
|
||||
"recording.widget.hint.saved_format": "{0} 저장됨",
|
||||
"recording.widget.save_picker_title": "녹음 파일 저장",
|
||||
"recording.widget.save_picker_type": "WAV 오디오",
|
||||
"study.environment.status_label": "환경 상태",
|
||||
"study.environment.status.initializing": "초기화 중",
|
||||
"study.environment.status.ready": "대기",
|
||||
"study.environment.status.quiet": "조용함",
|
||||
"study.environment.status.noisy": "시끄러움",
|
||||
"study.environment.status.paused": "일시정지됨",
|
||||
"study.environment.status.error": "오류",
|
||||
"study.environment.status.unsupported": "지원하지 않음",
|
||||
"study.environment.value.unavailable": "--",
|
||||
"study.environment.value.display_format": "{0:F1} dB",
|
||||
"study.environment.value.dbfs_format": "{0:F1} dBFS",
|
||||
"component.removable_storage": "이동식 저장소",
|
||||
"removable_storage.settings.desc": "연결된 USB 드라이브를 바탕화면에 표시하고 열기 및 꺼내기 작업을 제공합니다.",
|
||||
"removable_storage.settings.behavior_title": "동작",
|
||||
"removable_storage.settings.behavior_desc": "컴포넌트는 이동식 저장 장치를 자동으로 모니터링하고 가장 최근에 연결된 USB 드라이브를 우선 표시합니다.",
|
||||
"removable_storage.action.open": "열기",
|
||||
"removable_storage.action.eject": "꺼내기",
|
||||
"removable_storage.widget.default_name": "이동식 디스크",
|
||||
"removable_storage.widget.empty_title": "연결된 기기 없음",
|
||||
"removable_storage.widget.empty_subtitle": "USB 드라이브를 연결하면 여기에 자동으로 표시됩니다.",
|
||||
"removable_storage.widget.empty_hint": "이동식 기기를 연결하기 전까지 하단 버튼은 비활성화됩니다.",
|
||||
"removable_storage.widget.ready": "준비 완료, 바로 열거나 꺼낼 수 있습니다.",
|
||||
"removable_storage.widget.ejecting": "기기 꺼내는 중...",
|
||||
"removable_storage.widget.eject_failed": "이 기기를 꺼낼 수 없습니다. 사용 중인 파일을 닫은 후 다시 시도하세요.",
|
||||
"removable_storage.widget.open_failed": "이 기기를 열지 못했습니다.",
|
||||
"removable_storage.widget.refresh_failed": "이동식 저장소 목록 새로고침 실패.",
|
||||
"study.environment.settings.title": "환경 컴포넌트 설정",
|
||||
"study.environment.settings.desc": "오른쪽 실시간 소음 값 표시 내용을 구성합니다.",
|
||||
"study.environment.settings.show_display_db": "display dB 표시",
|
||||
"study.environment.settings.show_dbfs": "dBFS 표시",
|
||||
"study.environment.settings.hint": "최소 하나의 표시 방식을 활성화하세요.",
|
||||
"study.session_control.action.start": "공부 시간 시작",
|
||||
"study.session_control.action.stop": "공부 시간 종료",
|
||||
"study.session_control.idle_hint": "오른쪽 버튼을 클릭하여 시작",
|
||||
"study.session_control.report_preview": "보고서 미리보기",
|
||||
"study.session_control.report_confirm_hint": "오른쪽을 클릭하여 보기 종료 확인",
|
||||
"study.session_control.running_elapsed_format": "{0} 진행됨",
|
||||
"study.session_control.last_session_format": "마지막 시간 {0}",
|
||||
"study.session_control.start_failed": "시작 실패",
|
||||
"study.session_control.stop_failed": "종료 실패",
|
||||
"study.session_history.title": "기록 시간",
|
||||
"study.session_history.empty": "기록 시간 없음",
|
||||
"study.session_history.select_failed": "전환 실패",
|
||||
"study.session_history.rename_failed": "이름 변경 실패",
|
||||
"study.session_history.delete_failed": "삭제 실패",
|
||||
"study.session_history.rename_placeholder": "시간 이름 입력",
|
||||
"study.session_history.rename_confirm": "이름 변경 확인",
|
||||
"study.session_history.rename_cancel": "이름 변경 취소",
|
||||
"study.session_history.loading": "데이터 로딩 중...",
|
||||
"study.session_history.loaded": "데이터 로드됨",
|
||||
"study.session_history.duration_format": "{0:hh\\:mm\\:ss}",
|
||||
"study.session_history.meta_format": "{0} · 평균 {1:F1}",
|
||||
"study.session_history.action.view": "보기",
|
||||
"study.session_history.action.rename": "이름 변경",
|
||||
"study.session_history.action.delete": "삭제",
|
||||
"study.session_history.dialog.rename_title": "시간 이름 변경",
|
||||
"study.session_history.dialog.rename_message": "\"{0}\"의 새 이름을 입력하세요.",
|
||||
"study.session_history.dialog.delete_title": "시간 삭제",
|
||||
"study.session_history.dialog.delete_message": "\"{0}\"을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
|
||||
"study.session_history.dialog.delete_confirm": "삭제 확인",
|
||||
"study.noise_curve.value_format": "{0:F1} dB",
|
||||
"study.noise_curve.axis.now": "현재",
|
||||
"study.noise_distribution.title": "소음 레벨 분포",
|
||||
"study.noise_distribution.mode.realtime": "실시간",
|
||||
"study.noise_distribution.mode.session": "시간",
|
||||
"study.noise_distribution.summary.mainly_format": "주로: {0}",
|
||||
"study.noise_distribution.summary.latest_format": "최신: {0}",
|
||||
"study.noise_distribution.summary.compact_format": "주 {0} · 신 {1}",
|
||||
"study.noise_distribution.level.quiet": "조용함",
|
||||
"study.noise_distribution.level.normal": "보통",
|
||||
"study.noise_distribution.level.noisy": "시끄러움",
|
||||
"study.noise_distribution.level.extreme": "매우 시끄러움",
|
||||
"study.noise_distribution.axis.extreme": "매우 시끄러움",
|
||||
"study.noise_distribution.axis.noisy": "시끄러움",
|
||||
"study.noise_distribution.axis.normal": "보통",
|
||||
"study.noise_distribution.axis.quiet": "조용함",
|
||||
"study.noise_distribution.axis.now": "현재",
|
||||
"study.score_overview.title": "공부 점수",
|
||||
"study.score_overview.mode.realtime": "실시간",
|
||||
"study.score_overview.mode.session": "시간",
|
||||
"study.score_overview.current": "현재",
|
||||
"study.score_overview.average": "평균",
|
||||
"study.score_overview.minimum": "최저",
|
||||
"study.score_overview.maximum": "최고",
|
||||
"study.score_overview.average_short": "평",
|
||||
"study.score_overview.minimum_short": "저",
|
||||
"study.score_overview.maximum_short": "고",
|
||||
"study.score_overview.unavailable": "--",
|
||||
"study.deduction.title": "감점 원인",
|
||||
"study.deduction.mode.realtime": "실시간",
|
||||
"study.deduction.mode.session": "시간",
|
||||
"study.deduction.reason.sustained": "지속적 소음",
|
||||
"study.deduction.reason.time": "임계 초과 시간",
|
||||
"study.deduction.reason.segment": "방해 빈도",
|
||||
"study.deduction.reason.sustained_short": "지속",
|
||||
"study.deduction.reason.time_short": "시간",
|
||||
"study.deduction.reason.segment_short": "방해",
|
||||
"study.deduction.metric.sustained_format": "p50 {0:F1} dBFS",
|
||||
"study.deduction.metric.sustained_short_format": "p50 {0:F1}",
|
||||
"study.deduction.metric.time_format": "임계 초과 {0:F1}%",
|
||||
"study.deduction.metric.time_short_format": "{0:F1}%",
|
||||
"study.deduction.metric.segment_format": "{0:F1}회/분",
|
||||
"study.deduction.metric.segment_short_format": "{0:F1}/분",
|
||||
"study.deduction.loss_format": "-{0:F1}",
|
||||
"study.deduction.total_loss_format": "총 감점 -{0:F1}",
|
||||
"study.deduction.total_score_format": "점수 {0:F1}",
|
||||
"study.deduction.total_loss_unavailable": "총 감점 {0}",
|
||||
"study.deduction.total_score_unavailable": "점수 {0}",
|
||||
"study.deduction.unavailable": "--",
|
||||
"study.interrupt_density.title": "방해 밀도",
|
||||
"study.interrupt_density.mode.realtime": "실시간",
|
||||
"study.interrupt_density.mode.session": "시간",
|
||||
"study.interrupt_density.unit": "회/분",
|
||||
"study.interrupt_density.segment_count": "방해 횟수",
|
||||
"study.interrupt_density.segment_count_short": "횟수",
|
||||
"study.interrupt_density.duration": "통계 시간",
|
||||
"study.interrupt_density.duration_short": "시간",
|
||||
"study.interrupt_density.density_value_format": "{0:F1}",
|
||||
"study.interrupt_density.segment_count_value_format": "{0}",
|
||||
"study.interrupt_density.level_format": "방해 레벨: {0}",
|
||||
"study.interrupt_density.level.calm": "낮음",
|
||||
"study.interrupt_density.level.normal": "보통",
|
||||
"study.interrupt_density.level.frequent": "높음",
|
||||
"study.interrupt_density.level.severe": "매우 높음",
|
||||
"study.interrupt_density.threshold_format": "최대 감점 임계값 {0:F1}회/분",
|
||||
"study.interrupt_density.unavailable": "--",
|
||||
"desktop.add_page": "새 페이지 추가",
|
||||
"desktop.delete_page": "페이지 삭제",
|
||||
"placement.fill": "채우기",
|
||||
"placement.fit": "맞추기",
|
||||
"placement.stretch": "늘리기",
|
||||
"placement.center": "가운데",
|
||||
"placement.tile": "바둑판",
|
||||
"single_instance.notice.title": "앱이 이미 실행 중입니다",
|
||||
"single_instance.notice.description": "앱이 이미 실행 중이므로 여러 번 클릭하여 열 필요가 없습니다.",
|
||||
"single_instance.notice.button": "확인",
|
||||
"market.status.install_success_restart_format": "✓ 플러그인 '{0}' 설치 성공! 활성화하려면 앱을 재시작하세요.",
|
||||
"market.dialog.restart_message_format": "플러그인 '{0}'이(가) 성공적으로 설치되었습니다.\n\n이 플러그인을 사용하려면 앱을 즉시 재시작해야 합니다.\n\n지금 재시작하시겠습니까?"
|
||||
}
|
||||
@@ -7,7 +7,12 @@
|
||||
"tray.menu.restart": "重启应用",
|
||||
"tray.menu.exit": "退出应用",
|
||||
"button.back_to_windows": "回到Windows",
|
||||
"button.back_to_platform": "回到{0}",
|
||||
"tooltip.back_to_windows": "回到Windows",
|
||||
"tooltip.back_to_platform": "回到{0}",
|
||||
"platform.windows": "Windows",
|
||||
"platform.linux": "Linux",
|
||||
"platform.macos": "macOS",
|
||||
"tooltip.open_settings": "设置",
|
||||
"settings.title": "设置",
|
||||
"settings.shell.title": "设置",
|
||||
@@ -20,7 +25,7 @@
|
||||
"settings.nav.group_system": "系统",
|
||||
"settings.nav.group_extensions": "扩展",
|
||||
"settings.nav.wallpaper": "壁纸",
|
||||
"settings.nav.grid": "网格",
|
||||
"settings.nav.grid": "组件",
|
||||
"settings.nav.color": "颜色",
|
||||
"settings.nav.status_bar": "状态栏",
|
||||
"settings.nav.weather": "天气",
|
||||
@@ -31,13 +36,31 @@
|
||||
"settings.nav.plugins": "插件",
|
||||
"settings.nav.about": "关于",
|
||||
"settings.wallpaper.title": "壁纸",
|
||||
"settings.wallpaper.description": "选择图片或视频后可立即设为应用窗口壁纸。",
|
||||
"settings.wallpaper.description": "选择图片后可立即设为应用窗口壁纸。",
|
||||
"settings.wallpaper.current_label": "当前壁纸",
|
||||
"settings.wallpaper.type_label": "壁纸类型",
|
||||
"settings.wallpaper.type.image": "图片",
|
||||
"settings.wallpaper.type.video": "视频",
|
||||
"settings.wallpaper.type.solid_color": "纯色",
|
||||
"settings.wallpaper.type.system": "系统壁纸",
|
||||
"settings.wallpaper.system.label": "系统壁纸",
|
||||
"settings.wallpaper.system.unavailable": "无法读取系统壁纸",
|
||||
"settings.wallpaper.refresh_interval": "刷新频率",
|
||||
"settings.wallpaper.refresh_now": "立即刷新",
|
||||
"settings.wallpaper.refresh.30s": "30 秒",
|
||||
"settings.wallpaper.refresh.1m": "1 分钟",
|
||||
"settings.wallpaper.refresh.5m": "5 分钟",
|
||||
"settings.wallpaper.refresh.10m": "10 分钟",
|
||||
"settings.wallpaper.refresh.15m": "15 分钟",
|
||||
"settings.wallpaper.refresh.30m": "30 分钟",
|
||||
"settings.wallpaper.refresh.1h": "1 小时",
|
||||
"settings.wallpaper.refresh.2h": "2 小时",
|
||||
"settings.wallpaper.refresh.4h": "4 小时",
|
||||
"settings.wallpaper.refresh.8h": "8 小时",
|
||||
"settings.wallpaper.refresh.12h": "12 小时",
|
||||
"settings.wallpaper.refresh.24h": "24 小时",
|
||||
"settings.wallpaper.color_label": "壁纸颜色",
|
||||
"settings.wallpaper.custom_color_tooltip": "自定义颜色",
|
||||
"settings.wallpaper.custom_color_apply": "应用",
|
||||
"settings.wallpaper.placement_label": "显示方式",
|
||||
"settings.wallpaper.placement_desc": "调整图像在桌面上的填充方式。",
|
||||
"settings.wallpaper.pick_button": "选择文件",
|
||||
@@ -46,20 +69,14 @@
|
||||
"settings.wallpaper.storage_unavailable": "存储提供器不可用。",
|
||||
"settings.wallpaper.import_failed": "导入壁纸文件失败。",
|
||||
"settings.wallpaper.image_applied": "图片壁纸已应用。",
|
||||
"settings.wallpaper.video_applied": "视频壁纸已应用。",
|
||||
"settings.wallpaper.unsupported_file": "所选文件类型不受支持。",
|
||||
"settings.wallpaper.apply_failed_format": "应用壁纸失败:{0}",
|
||||
"settings.wallpaper.mode_format": "壁纸模式:{0}。",
|
||||
"settings.wallpaper.video_mode": "视频壁纸使用自动填充模式。",
|
||||
"settings.wallpaper.cleared": "背景已恢复为纯色。",
|
||||
"settings.wallpaper.default_status": "当前使用纯色背景。",
|
||||
"settings.wallpaper.saved_not_found": "未找到已保存的壁纸文件,已使用纯色背景。",
|
||||
"settings.wallpaper.restored": "已恢复保存的壁纸。",
|
||||
"settings.wallpaper.video_restored": "已恢复保存的视频壁纸。",
|
||||
"settings.wallpaper.restore_failed": "恢复已保存壁纸失败,已使用纯色背景。",
|
||||
"settings.wallpaper.video_not_found": "未找到视频壁纸文件。",
|
||||
"settings.wallpaper.video_player_unavailable": "视频播放器不可用。",
|
||||
"settings.wallpaper.video_play_failed_format": "播放视频壁纸失败:{0}",
|
||||
"settings.grid.title": "网格布局",
|
||||
"settings.grid.description": "每个组件至少占用一个格子(最小 1x1)。",
|
||||
"settings.grid.short_side_label": "短边格数",
|
||||
@@ -85,12 +102,13 @@
|
||||
"settings.color.theme_ready_format": "主题色已就绪:{0}。",
|
||||
"settings.color.theme_applied_format": "{0}主题色已应用:{1}。",
|
||||
"settings.color.theme_updated_wallpaper": "壁纸已更新,莫奈色已刷新。",
|
||||
"settings.color.theme_updated_video": "视频壁纸已更新,主题色已刷新。",
|
||||
"settings.color.theme_cleared_wallpaper": "壁纸已清除,莫奈色已刷新。",
|
||||
"settings.status_bar.title": "状态栏",
|
||||
"settings.status_bar.description": "选择顶部状态栏显示的组件。",
|
||||
"settings.status_bar.clock_header": "时间组件",
|
||||
"settings.status_bar.clock_description": "在顶部状态栏显示时钟。",
|
||||
"settings.status_bar.clock_transparent_background_label": "透明背景",
|
||||
"settings.status_bar.clock_transparent_background_desc": "移除胶囊背景,仅保留时钟文字。",
|
||||
"settings.status_bar.spacing_header": "组件间距",
|
||||
"settings.status_bar.spacing_desc": "调整状态栏组件之间的间距。",
|
||||
"settings.status_bar.spacing_mode_compact": "紧凑",
|
||||
@@ -104,6 +122,11 @@
|
||||
"settings.privacy.crash_upload_description": "帮助我们提高应用稳定性。",
|
||||
"settings.privacy.usage_upload_title": "匿名上传使用数据",
|
||||
"settings.privacy.usage_upload_description": "帮助我们改善应用功能。",
|
||||
"settings.privacy.device_id_title": "设备标识符",
|
||||
"settings.privacy.device_id_description": "此设备的唯一标识符。点击刷新以重新生成。",
|
||||
"settings.privacy.refresh_device_id": "刷新",
|
||||
"settings.privacy.policy_hint_prefix": "了解更多详情,请",
|
||||
"settings.privacy.view_policy": "查看我们的隐私政策",
|
||||
"settings.weather.title": "天气",
|
||||
"settings.weather.description": "配置天气位置、小米天气预览和启动时的位置刷新行为。",
|
||||
"settings.weather.location_source_header": "位置来源",
|
||||
@@ -210,7 +233,14 @@
|
||||
"schedule.settings.unnamed": "未命名课表",
|
||||
"schedule.settings.delete": "删除",
|
||||
"schedule.settings.picker_title": "选择 ClassIsland 课表文件",
|
||||
"schedule.settings.picker_file_type": "ClassIsland CSES 课表",
|
||||
"schedule.settings.picker_file_type.all": "ClassIsland 课表文件",
|
||||
"schedule.settings.picker_file_type.json": "ClassIsland 档案 (JSON)",
|
||||
"schedule.settings.picker_file_type.cses": "CSES 课表 (YAML)",
|
||||
"schedule.settings.semester.title": "学期设置",
|
||||
"schedule.settings.semester.start_date": "学期开始日期",
|
||||
"schedule.settings.semester.week_cycle": "周循环",
|
||||
"schedule.settings.semester.week_cycle_desc": "设置多周课表轮换周期,用于计算当前是第几周。",
|
||||
"schedule.settings.semester.week_cycle_format": "{0} 周轮换",
|
||||
"worldclock.settings.title": "世界时钟设置",
|
||||
"worldclock.settings.desc": "分别为四个时钟选择时区。",
|
||||
"worldclock.settings.clock_1": "时钟 1",
|
||||
@@ -241,6 +271,7 @@
|
||||
"settings.region.language_label": "语言",
|
||||
"settings.region.language_zh": "中文",
|
||||
"settings.region.language_en": "英文",
|
||||
"settings.region.language_ja": "日文",
|
||||
"settings.region.timezone_header": "时区",
|
||||
"settings.region.timezone_desc": "选择时区。时钟与日历组件会使用该时区。",
|
||||
"settings.region.applied_format": "语言已切换为:{0}",
|
||||
@@ -295,9 +326,18 @@
|
||||
"settings.status_bar.clock_format_label": "时钟格式",
|
||||
"settings.status_bar.clock_format.hm": "时:分",
|
||||
"settings.status_bar.clock_format.hms": "时:分:秒",
|
||||
"settings.components.title": "网格",
|
||||
"settings.components.description": "调整桌面网格与布局。",
|
||||
"settings.components.grid_header": "网格布局",
|
||||
"settings.components.title": "组件",
|
||||
"settings.components.description": "调整组件布局与圆角设计。",
|
||||
"settings.components.grid_header": "网格设置",
|
||||
"settings.components.header": "网格设置",
|
||||
"settings.components.short_side_label": "短边格数",
|
||||
"settings.components.edge_inset_label": "屏幕边距",
|
||||
"settings.components.spacing_label": "组件间距",
|
||||
"settings.components.spacing_compact": "紧凑",
|
||||
"settings.components.spacing_relaxed": "宽松",
|
||||
"settings.components.corner_radius.header": "圆角设计",
|
||||
"settings.components.corner_radius.label": "组件圆角",
|
||||
"settings.components.corner_radius.description": "将组件容器圆角从直角连续调到接近胶囊的形态,并随圆角增大同步扩展内部安全区。",
|
||||
"settings.update.title": "更新",
|
||||
"settings.update.current_version_label": "当前版本",
|
||||
"settings.update.latest_version_label": "最新发布",
|
||||
@@ -373,6 +413,11 @@
|
||||
"settings.update.channel_preview_desc": "预览版可能包含更早的新功能,但稳定性可能较低。",
|
||||
"settings.update.download_threads_label": "下载线程数",
|
||||
"settings.update.download_threads_desc": "设置应用更新安装包使用的并行下载线程数。",
|
||||
"settings.update.force_check_label": "强制检查更新",
|
||||
"settings.update.force_check_desc": "强制从 GitHub 获取最新版本,忽略版本比较。",
|
||||
"settings.update.status_force_checking": "正在强制检查 GitHub Release...",
|
||||
"settings.update.status_force_no_asset": "已找到发布版本,但没有可用的兼容安装包。",
|
||||
"settings.update.status_force_available_format": "发布版本 {0} 可用,点击“下载并安装”继续。",
|
||||
"settings.update.install_now_button": "立即安装",
|
||||
"settings.update.status_downloaded_confirm": "更新已下载完成,请查看并选择安装时机。",
|
||||
"settings.update.status_downloaded_exit": "更新已下载完成,将在你退出应用时安装。",
|
||||
@@ -393,7 +438,6 @@
|
||||
"settings.footer": "LanMountainDesktop 设置",
|
||||
"filepicker.title": "选择壁纸",
|
||||
"filepicker.image_files": "图片文件",
|
||||
"filepicker.video_files": "视频文件",
|
||||
"common.day": "日间",
|
||||
"common.night": "夜间",
|
||||
"common.back": "返回",
|
||||
@@ -403,6 +447,7 @@
|
||||
"common.monet": "莫奈",
|
||||
"desktop.page_index_format": "桌面 {0}",
|
||||
"launcher.title": "应用启动台",
|
||||
"launcher.folder": "文件夹",
|
||||
"launcher.subtitle": "按 Windows 开始菜单结构显示所有应用与文件夹",
|
||||
"launcher.subtitle_linux": "显示从 Linux .desktop 条目扫描到的已安装应用",
|
||||
"launcher.empty": "未找到开始菜单条目。",
|
||||
@@ -431,8 +476,8 @@
|
||||
"settings.plugins.refresh_button": "刷新插件",
|
||||
"settings.plugins.refresh_success_installed_format": "已加载 {0} 个已安装插件。",
|
||||
"settings.plugins.refresh_success_format": "已加载 {0} 个已安装插件和 {1} 个市场条目。",
|
||||
"settings.plugins.refresh_failed": "加载插件市场索引失败。",
|
||||
"settings.plugins.marketplace_header": "插件市场",
|
||||
"settings.plugins.refresh_failed": "加载插件目录索引失败。",
|
||||
"settings.plugins.marketplace_header": "插件目录",
|
||||
"settings.plugins.marketplace_empty": "当前没有可用的市场插件。",
|
||||
"settings.plugins.delete_button_short": "删除",
|
||||
"settings.plugins.install_button_short": "安装",
|
||||
@@ -479,10 +524,10 @@
|
||||
"settings.plugins.source_manifest": "散装清单",
|
||||
"settings.plugins.subtitle_format": "{0} | {1} | {2}",
|
||||
"settings.plugins.detail_format": "设置页:{0} | 组件:{1}",
|
||||
"settings.nav.plugin_market": "插件市场",
|
||||
"settings.plugin_market.title": "插件市场",
|
||||
"settings.plugin_market.subtitle": "浏览来自 LanAirApp 官方源的插件,并将安装暂存到本地。",
|
||||
"settings.plugin_market.unavailable": "插件运行时不可用,暂时无法打开官方市场。",
|
||||
"settings.nav.plugin_catalog": "插件目录",
|
||||
"settings.plugin_catalog.title": "插件目录",
|
||||
"settings.plugin_catalog.subtitle": "浏览来自 LanAirApp 官方源的插件,并将安装暂存到本地。",
|
||||
"settings.plugin_catalog.unavailable": "插件运行时不可用,暂时无法打开官方目录。",
|
||||
"settings.update.status_idle": "尚未执行更新检查。",
|
||||
"settings.update.status_preferences_saved": "更新偏好已保存。",
|
||||
"settings.update.status_check_failed": "检查更新失败。",
|
||||
@@ -491,15 +536,15 @@
|
||||
"settings.window.drawer_default": "详情",
|
||||
"market.toolbar.search_placeholder": "搜索插件",
|
||||
"market.toolbar.refresh": "刷新",
|
||||
"market.status.loading": "正在加载官方插件市场...",
|
||||
"market.status.loading": "正在加载官方插件目录...",
|
||||
"market.status.loaded_network_format": "已从官方源加载 {0} 个插件。",
|
||||
"market.status.loaded_cache_format": "官方源暂时不可用,已从缓存加载 {0} 个插件。原因:{1}",
|
||||
"market.status.load_failed_format": "加载插件市场失败:{0}",
|
||||
"market.status.load_failed_format": "加载插件目录失败:{0}",
|
||||
"market.status.installing_format": "正在下载并暂存插件“{0}”...",
|
||||
"market.status.install_success_format": "插件“{0}”已暂存完成。重启应用后生效。",
|
||||
"market.status.install_failed_format": "安装插件失败:{0}",
|
||||
"market.status.host_incompatible_format": "当前宿主版本过低,至少需要 {0}。",
|
||||
"market.list.empty": "插件市场尚未加载。",
|
||||
"market.list.empty": "插件目录尚未加载。",
|
||||
"market.list.no_results": "没有匹配当前搜索的插件。",
|
||||
"market.card.subtitle_format": "{0} | v{1}",
|
||||
"market.card.loaded": "已加载",
|
||||
@@ -559,6 +604,7 @@
|
||||
"component_category.info": "信息推荐",
|
||||
"component_category.calculator": "计算器",
|
||||
"component_category.study": "自习",
|
||||
"component_category.file": "文件",
|
||||
"component.date": "日历",
|
||||
"component.month_calendar": "月历",
|
||||
"component.lunar_calendar": "农历",
|
||||
@@ -587,6 +633,18 @@
|
||||
"component.blackboard_landscape": "横向小黑板",
|
||||
"component.browser": "浏览器",
|
||||
"component.office_recent_documents": "最近文档",
|
||||
"whiteboard.settings.desc": "每个小黑板都会独立保存自己的笔记历史。",
|
||||
"whiteboard.settings.retention.title": "笔记保留时间",
|
||||
"whiteboard.settings.retention.desc": "选择这个小黑板在过期笔记被自动删除前,应当保留已保存笔记多久。",
|
||||
"whiteboard.settings.retention.option": "{0} 天",
|
||||
"whiteboard.settings.instance_scope": "这个保留时间设置会按每个小黑板组件实例单独存储。",
|
||||
"office_recent_documents.settings.desc": "选择此小组件需要扫描的 Windows 和 Office 最近文档来源。",
|
||||
"office_recent_documents.settings.sources_title": "最近文档来源",
|
||||
"office_recent_documents.settings.sources_desc": "可以同时选择多个来源。勾选注册表来源时,还会保留 Office interop 的 MRU 回退。",
|
||||
"office_recent_documents.settings.source.registry": "Office 注册表 MRU",
|
||||
"office_recent_documents.settings.source.recent_folders": "Windows 最近文件夹",
|
||||
"office_recent_documents.settings.source.jump_lists": "Windows 跳转列表",
|
||||
"office_recent_documents.settings.hint": "如果关闭全部来源,此小组件会保持空白,直到再次至少启用一个来源。",
|
||||
"component.holiday_calendar": "节假日日历",
|
||||
"component.study_environment": "环境",
|
||||
"component.study_session_control": "自习时段控制",
|
||||
@@ -783,6 +841,21 @@
|
||||
"study.environment.value.unavailable": "--",
|
||||
"study.environment.value.display_format": "{0:F1} dB",
|
||||
"study.environment.value.dbfs_format": "{0:F1} dBFS",
|
||||
"component.removable_storage": "可移动存储",
|
||||
"removable_storage.settings.desc": "在桌面上显示已连接的 U 盘,并提供打开与弹出操作。",
|
||||
"removable_storage.settings.behavior_title": "行为",
|
||||
"removable_storage.settings.behavior_desc": "组件会自动监听可移动存储设备,并优先显示最新插入的 U 盘。",
|
||||
"removable_storage.action.open": "打开",
|
||||
"removable_storage.action.eject": "弹出",
|
||||
"removable_storage.widget.default_name": "可移动磁盘",
|
||||
"removable_storage.widget.empty_title": "未插入设备",
|
||||
"removable_storage.widget.empty_subtitle": "插入 U 盘后会自动显示在这里。",
|
||||
"removable_storage.widget.empty_hint": "在插入可移动设备之前,底部按钮会保持置灰不可点击。",
|
||||
"removable_storage.widget.ready": "已准备好,可直接打开或弹出。",
|
||||
"removable_storage.widget.ejecting": "正在弹出设备...",
|
||||
"removable_storage.widget.eject_failed": "无法弹出该设备,请先关闭正在占用它的文件后再试。",
|
||||
"removable_storage.widget.open_failed": "打开该设备失败。",
|
||||
"removable_storage.widget.refresh_failed": "刷新可移动存储列表失败。",
|
||||
"study.environment.settings.title": "环境组件设置",
|
||||
"study.environment.settings.desc": "配置右侧实时噪音值显示内容。",
|
||||
"study.environment.settings.show_display_db": "显示 display dB",
|
||||
@@ -885,6 +958,10 @@
|
||||
"study.interrupt_density.unavailable": "--",
|
||||
"desktop.add_page": "新增页面",
|
||||
"desktop.delete_page": "删除页面",
|
||||
"desktop.delete_page_confirm.title": "确认删除页面",
|
||||
"desktop.delete_page_confirm.message": "确定要删除当前页面吗?\n\n此操作将删除当前页面上的所有组件,且无法撤销。",
|
||||
"desktop.delete_page_confirm.primary": "删除",
|
||||
"desktop.delete_page_confirm.close": "取消",
|
||||
"placement.fill": "填充",
|
||||
"placement.fit": "适应",
|
||||
"placement.stretch": "拉伸",
|
||||
@@ -892,5 +969,7 @@
|
||||
"placement.tile": "平铺",
|
||||
"single_instance.notice.title": "应用已经运行",
|
||||
"single_instance.notice.description": "应用已经运行,无需多次点击打开。",
|
||||
"single_instance.notice.button": "确定"
|
||||
"single_instance.notice.button": "确定",
|
||||
"market.status.install_success_restart_format": "✓ 插件'{0}'安装成功!请重启应用以激活它。",
|
||||
"market.dialog.restart_message_format": "插件'{0}'已成功安装。\n\n要使用此插件,您需要立即重启应用。\n\n是否立即重启?"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user