mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc4d0c4cd8 |
42
.trae/specs/localization-fix/checklist.md
Normal file
42
.trae/specs/localization-fix/checklist.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 本地化修复 Checklist
|
||||||
|
|
||||||
|
## MainWindow 修复
|
||||||
|
- [ ] `TaskbarProfileDisplayNameTextBlock.Text` 在中文下显示"用户"(或保持动态)
|
||||||
|
- [ ] `TaskbarProfileSettingsActionTextBlock.Text` 在中文下显示"设置"
|
||||||
|
- [ ] `TaskbarProfileDesktopEditActionTextBlock.Text` 在中文下显示"桌面编辑"
|
||||||
|
- [ ] `TaskbarProfilePowerActionTextBlock.Text` 在中文下显示"电源"
|
||||||
|
- [ ] `TaskbarPowerBackTextBlock.Text` 在中文下显示"返回"
|
||||||
|
- [ ] `TaskbarPowerTitleTextBlock.Text` 在中文下显示"电源"
|
||||||
|
- [ ] `PowerShutdownTextBlock.Text` 在中文下显示"关机"
|
||||||
|
- [ ] `PowerRestartTextBlock.Text` 在中文下显示"重启"
|
||||||
|
- [ ] `PowerLogoutTextBlock.Text` 在中文下显示"注销"
|
||||||
|
- [ ] `PowerSleepTextBlock.Text` 在中文下显示"睡眠"
|
||||||
|
- [ ] `PowerLockTextBlock.Text` 在中文下显示"锁定屏幕"
|
||||||
|
- [ ] `ComponentLibraryTitleTextBlock.Text` 在中文下显示"桌面编辑"
|
||||||
|
- [ ] `ComponentLibraryEmptyTextBlock.Text` 在中文下显示"左右滑动选择类别,点击进入,然后拖动组件到桌面放置。"
|
||||||
|
- [ ] `ComponentLibraryBackTextBlock.Text` 在中文下显示"返回"
|
||||||
|
- [ ] `ComponentLibraryCollapsedChipTextBlock.Text` 在中文下显示"桌面编辑"
|
||||||
|
|
||||||
|
## Launcher 修复
|
||||||
|
- [ ] `SplashWindow` 在中文下显示中文启动文本
|
||||||
|
- [ ] `DataLocationPromptWindow` 在中文下全部显示中文
|
||||||
|
- [ ] `ErrorWindow` 在中文下全部显示中文
|
||||||
|
- [ ] `LoadingDetailsWindow` 在中文下全部显示中文
|
||||||
|
- [ ] `UpdateWindow` 在中文下显示中文标题
|
||||||
|
|
||||||
|
## 组件修复
|
||||||
|
- [ ] `BrowserWidget` 在中文下显示"浏览器运行时不可用"
|
||||||
|
- [ ] `WhiteboardWidget` 工具提示在中文下显示"笔"、"橡皮擦"、"清空"、"导出 SVG"
|
||||||
|
- [ ] `HolidayCalendarWidget` 在中文下显示"节假日倒计时"、"天"
|
||||||
|
- [ ] `BilibiliHotSearchWidget` 在中文下显示"热门话题"
|
||||||
|
- [ ] `WallpaperSettingsPage` 自定义颜色 Tooltip 在中文下显示"自定义颜色"
|
||||||
|
|
||||||
|
## 资源文件
|
||||||
|
- [ ] `zh-CN.json` 包含所有新增键值
|
||||||
|
- [ ] `en-US.json` 包含所有新增键值
|
||||||
|
- [ ] Launcher 本地化文件包含所有新增键值
|
||||||
|
|
||||||
|
## 构建与质量
|
||||||
|
- [ ] `dotnet build LanMountainDesktop.slnx -c Debug` 编译通过,无错误
|
||||||
|
- [ ] 无新增警告
|
||||||
|
- [ ] 无遗漏的硬编码英文(通过 `grep -r 'Text="[a-zA-Z]'` 等检查)
|
||||||
85
.trae/specs/localization-fix/spec.md
Normal file
85
.trae/specs/localization-fix/spec.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# 本地化修复 Spec
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
- 项目在中文设置下,多处 UI 仍显示英文。
|
||||||
|
- 主要问题集中在:
|
||||||
|
1. `MainWindow.axaml` 中任务栏头像弹窗、电源菜单、组件库等文本硬编码为英文,且未被 `ApplyLocalization()` 覆盖。
|
||||||
|
2. `LanMountainDesktop.Launcher` 的所有视图完全没有接入本地化系统。
|
||||||
|
3. 部分组件(BrowserWidget、WhiteboardWidget、HolidayCalendarWidget 等)存在未覆盖的硬编码英文。
|
||||||
|
4. 少量设置页面 Tooltip 硬编码英文。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
### 1. MainWindow.axaml 硬编码修复
|
||||||
|
将以下硬编码文本改为由 `ApplyLocalization()` 通过 `L()` 动态设置:
|
||||||
|
- 任务栏头像弹窗:`User` → `power.user` / `Settings` → `settings.title` / `Edit Desktop` → `button.component_library` / `Power` → `power.title`
|
||||||
|
- 电源菜单:`Back` → `common.back` / `Power` → `power.title` / `Shutdown` → `power.shutdown` / `Restart` → `power.restart` / `Log Out` → `power.logout` / `Sleep` → `power.sleep` / `Lock Screen` → `power.lock_screen`
|
||||||
|
- 组件库:`Widgets` → `component_library.title` / `Back` → `common.back` / `No components.` → `component_library.empty`
|
||||||
|
- 悬浮芯片:`Widgets` → `component_library.title`
|
||||||
|
|
||||||
|
### 2. Launcher 视图本地化
|
||||||
|
为 `LanMountainDesktop.Launcher/Views/` 下的窗口引入独立本地化机制(复用 `LocalizationService` 或内嵌资源字典):
|
||||||
|
- `SplashWindow.axaml`:`LanMountain Desktop`、`Initializing...`
|
||||||
|
- `DataLocationPromptWindow.axaml`:全部文本
|
||||||
|
- `ErrorWindow.axaml`:全部文本
|
||||||
|
- `LoadingDetailsWindow.axaml`:全部文本
|
||||||
|
- `UpdateWindow.axaml`:`Update`
|
||||||
|
|
||||||
|
### 3. 组件硬编码修复
|
||||||
|
- `BrowserWidget.axaml`:`Browser runtime unavailable.` → 新增键 `browser.widget.unavailable`
|
||||||
|
- `WhiteboardWidget.axaml`:`Pen` / `Eraser` / `Clear` / `Export SVG` → 新增键 `whiteboard.tool.pen` 等
|
||||||
|
- `HolidayCalendarWidget.axaml`:`Holiday countdown` / `Days` → 新增键 `holiday.widget.title` / `holiday.widget.days`
|
||||||
|
- `BilibiliHotSearchWidget.axaml`:`Trending Topic` → 新增键 `bilihot.widget.trending_topic`
|
||||||
|
- `WallpaperSettingsPage.axaml`:`Custom color` Tooltip → 复用 `settings.wallpaper.custom_color_tooltip`
|
||||||
|
|
||||||
|
### 4. 本地化资源文件补充
|
||||||
|
在 `zh-CN.json` 和 `en-US.json` 中补充上述新增键值。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected code:
|
||||||
|
- `LanMountainDesktop/Views/MainWindow.axaml`
|
||||||
|
- `LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs`
|
||||||
|
- `LanMountainDesktop.Launcher/Views/*.axaml`(多个文件)
|
||||||
|
- `LanMountainDesktop/Views/Components/BrowserWidget.axaml`
|
||||||
|
- `LanMountainDesktop/Views/Components/WhiteboardWidget.axaml`
|
||||||
|
- `LanMountainDesktop/Views/Components/HolidayCalendarWidget.axaml`
|
||||||
|
- `LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml`
|
||||||
|
- `LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml`
|
||||||
|
- `LanMountainDesktop/Localization/zh-CN.json`
|
||||||
|
- `LanMountainDesktop/Localization/en-US.json`
|
||||||
|
- Affected behavior:
|
||||||
|
- 中文设置下上述位置将正确显示中文。
|
||||||
|
- Launcher 各窗口将支持中英文切换。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: MainWindow 任务栏弹窗与电源菜单本地化
|
||||||
|
系统 SHALL 在 `ApplyLocalization()` 中覆盖任务栏头像弹窗和电源菜单的所有文本。
|
||||||
|
|
||||||
|
#### Scenario: 中文设置下打开任务栏弹窗
|
||||||
|
- **WHEN** 语言设置为中文
|
||||||
|
- **THEN** 弹窗中显示"设置"、"桌面编辑"、"电源"等中文文本
|
||||||
|
- **AND THEN** 电源菜单中显示"返回"、"关机"、"重启"、"注销"、"睡眠"、"锁定屏幕"等中文文本
|
||||||
|
|
||||||
|
### Requirement: Launcher 窗口本地化
|
||||||
|
系统 SHALL 让 Launcher 的所有窗口文本通过本地化服务获取。
|
||||||
|
|
||||||
|
#### Scenario: 中文设置下启动应用
|
||||||
|
- **WHEN** 语言设置为中文
|
||||||
|
- **THEN** SplashWindow 显示中文启动文本
|
||||||
|
- **AND THEN** 数据位置选择、错误页、加载详情页等显示中文
|
||||||
|
|
||||||
|
### Requirement: 组件与设置页硬编码修复
|
||||||
|
系统 SHALL 移除或覆盖所有组件和设置页中的英文硬编码文本。
|
||||||
|
|
||||||
|
#### Scenario: 中文设置下查看各组件
|
||||||
|
- **WHEN** 语言设置为中文
|
||||||
|
- **THEN** BrowserWidget 显示"浏览器运行时不可用"
|
||||||
|
- **AND THEN** WhiteboardWidget 工具提示显示"笔"、"橡皮擦"、"清空"、"导出 SVG"
|
||||||
|
- **AND THEN** HolidayCalendarWidget 显示"节假日倒计时"、"天"
|
||||||
|
- **AND THEN** BilibiliHotSearchWidget 显示"热门话题"
|
||||||
|
- **AND THEN** 壁纸设置页自定义颜色 Tooltip 显示"自定义颜色"
|
||||||
39
.trae/specs/localization-fix/tasks.md
Normal file
39
.trae/specs/localization-fix/tasks.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 本地化修复 Tasks
|
||||||
|
|
||||||
|
## Task 1: MainWindow.axaml 硬编码文本移除与代码覆盖
|
||||||
|
- [ ] 1.1 在 `MainWindow.axaml` 中,将任务栏头像弹窗的 `User`、`Settings`、`Edit Desktop`、`Power` 的 `Text` 属性改为空或绑定(保留 x:Name)
|
||||||
|
- [ ] 1.2 在 `MainWindow.axaml` 中,将电源菜单的 `Back`、`Power`、`Shutdown`、`Restart`、`Log Out`、`Sleep`、`Lock Screen` 的 `Text` 属性改为空或绑定
|
||||||
|
- [ ] 1.3 在 `MainWindow.axaml` 中,将组件库的 `Widgets`、`Back`、`No components.` 的 `Text` 属性改为空或绑定
|
||||||
|
- [ ] 1.4 在 `MainWindow.axaml` 中,将悬浮芯片的 `Widgets` 的 `Text` 属性改为空或绑定
|
||||||
|
- [ ] 1.5 在 `MainWindow.SettingsHardCut.Stubs.cs` 的 `ApplyLocalization()` 中补充上述所有控件的 `L()` 赋值
|
||||||
|
|
||||||
|
## Task 2: Launcher 视图本地化
|
||||||
|
- [ ] 2.1 在 `LanMountainDesktop.Launcher` 中引入 `LocalizationService`(或共享主应用服务)
|
||||||
|
- [ ] 2.2 为 Launcher 创建独立的 `Localization/` 目录和 `zh-CN.json` / `en-US.json`
|
||||||
|
- [ ] 2.3 修改 `SplashWindow.axaml`:将 `LanMountain Desktop`、`Initializing...` 改为动态绑定
|
||||||
|
- [ ] 2.4 修改 `DataLocationPromptWindow.axaml`:将所有文本改为动态绑定
|
||||||
|
- [ ] 2.5 修改 `ErrorWindow.axaml`:将所有文本改为动态绑定
|
||||||
|
- [ ] 2.6 修改 `LoadingDetailsWindow.axaml`:将所有文本改为动态绑定
|
||||||
|
- [ ] 2.7 修改 `UpdateWindow.axaml`:将 `Update` 改为动态绑定
|
||||||
|
- [ ] 2.8 在 Launcher 启动流程中初始化语言设置
|
||||||
|
|
||||||
|
## Task 3: 组件硬编码修复
|
||||||
|
- [ ] 3.1 `BrowserWidget.axaml`:将 `Browser runtime unavailable.` 改为绑定,并在代码后置中通过 `L()` 设置
|
||||||
|
- [ ] 3.2 `WhiteboardWidget.axaml`:将 `Pen`、`Eraser`、`Clear`、`Export SVG` Tooltip 改为绑定,并在代码后置中通过 `L()` 设置
|
||||||
|
- [ ] 3.3 `HolidayCalendarWidget.axaml`:将 `Holiday countdown`、`Days` 改为绑定,并在代码后置中通过 `L()` 设置
|
||||||
|
- [ ] 3.4 `BilibiliHotSearchWidget.axaml`:将 `Trending Topic` 改为绑定,并在代码后置中通过 `L()` 设置
|
||||||
|
- [ ] 3.5 `WallpaperSettingsPage.axaml`:将 `Custom color` Tooltip 改为绑定到 `settings.wallpaper.custom_color_tooltip`
|
||||||
|
|
||||||
|
## Task 4: 本地化资源文件补充
|
||||||
|
- [ ] 4.1 在 `zh-CN.json` 中补充以下键值:
|
||||||
|
- `browser.widget.unavailable`
|
||||||
|
- `whiteboard.tool.pen`、`whiteboard.tool.eraser`、`whiteboard.tool.clear`、`whiteboard.tool.export_svg`
|
||||||
|
- `holiday.widget.title`、`holiday.widget.days`
|
||||||
|
- `bilihot.widget.trending_topic`
|
||||||
|
- `power.user`(或复用现有键)
|
||||||
|
- [ ] 4.2 在 `en-US.json` 中补充上述键值的英文版本
|
||||||
|
- [ ] 4.3 为 Launcher 创建独立的本地化 JSON 文件并填充中英文
|
||||||
|
|
||||||
|
## Task 5: 验证
|
||||||
|
- [ ] 5.1 执行 `dotnet build LanMountainDesktop.slnx -c Debug` 确保编译通过
|
||||||
|
- [ ] 5.2 检查是否有遗漏的硬编码英文(通过正则搜索)
|
||||||
@@ -9,8 +9,10 @@ namespace LanMountainDesktop.Launcher.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class PluginInstallerService
|
internal sealed class PluginInstallerService
|
||||||
{
|
{
|
||||||
private const string ManifestFileName = "manifest.json";
|
private const string ManifestFileName = "plugin.json";
|
||||||
private const string PackageFileExtension = ".lmdp";
|
private const string LegacyManifestFileName = "manifest.json";
|
||||||
|
private const string PackageFileExtension = ".laapp";
|
||||||
|
private const string LegacyPackageFileExtension = ".lmdp";
|
||||||
private const string RuntimeDirectoryName = "runtime";
|
private const string RuntimeDirectoryName = "runtime";
|
||||||
|
|
||||||
private static readonly TimeSpan[] RetryDelays =
|
private static readonly TimeSpan[] RetryDelays =
|
||||||
@@ -114,14 +116,16 @@ internal sealed class PluginInstallerService
|
|||||||
public PluginManifest ReadManifestFromPackage(string packagePath)
|
public PluginManifest ReadManifestFromPackage(string packagePath)
|
||||||
{
|
{
|
||||||
using var archive = ZipFile.OpenRead(packagePath);
|
using var archive = ZipFile.OpenRead(packagePath);
|
||||||
var entries = archive.Entries
|
var entries = FindManifestEntries(archive, ManifestFileName);
|
||||||
.Where(entry => string.Equals(entry.Name, ManifestFileName, StringComparison.OrdinalIgnoreCase))
|
if (entries.Length == 0)
|
||||||
.ToArray();
|
{
|
||||||
|
entries = FindManifestEntries(archive, LegacyManifestFileName);
|
||||||
|
}
|
||||||
|
|
||||||
if (entries.Length == 0)
|
if (entries.Length == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Plugin package '{packagePath}' does not contain '{ManifestFileName}'.");
|
$"Plugin package '{packagePath}' does not contain '{ManifestFileName}' or '{LegacyManifestFileName}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entries.Length > 1)
|
if (entries.Length > 1)
|
||||||
@@ -141,6 +145,13 @@ internal sealed class PluginInstallerService
|
|||||||
return manifest;
|
return manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ZipArchiveEntry[] FindManifestEntries(ZipArchive archive, string manifestFileName)
|
||||||
|
{
|
||||||
|
return archive.Entries
|
||||||
|
.Where(entry => string.Equals(entry.Name, manifestFileName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
private void RemoveExistingPluginPackages(string pluginsDirectory, string pluginId, string destinationPath, string stagingPath)
|
private void RemoveExistingPluginPackages(string pluginsDirectory, string pluginId, string destinationPath, string stagingPath)
|
||||||
{
|
{
|
||||||
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(pluginsDirectory), RuntimeDirectoryName));
|
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(pluginsDirectory), RuntimeDirectoryName));
|
||||||
@@ -148,8 +159,11 @@ internal sealed class PluginInstallerService
|
|||||||
Directory.CreateDirectory(pendingDeletionDir);
|
Directory.CreateDirectory(pendingDeletionDir);
|
||||||
|
|
||||||
foreach (var existingPackagePath in Directory
|
foreach (var existingPackagePath in Directory
|
||||||
.EnumerateFiles(pluginsDirectory, "*" + PackageFileExtension, SearchOption.AllDirectories)
|
.EnumerateFiles(pluginsDirectory, "*", SearchOption.AllDirectories)
|
||||||
.Select(Path.GetFullPath)
|
.Select(Path.GetFullPath)
|
||||||
|
.Where(path =>
|
||||||
|
path.EndsWith(PackageFileExtension, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
path.EndsWith(LegacyPackageFileExtension, StringComparison.OrdinalIgnoreCase))
|
||||||
.Where(path => !path.StartsWith(runtimeRootDirectory, StringComparison.OrdinalIgnoreCase)))
|
.Where(path => !path.StartsWith(runtimeRootDirectory, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
Official `dotnet new` template package for LanMountainDesktop plugins.
|
Official `dotnet new` template package for LanMountainDesktop plugins.
|
||||||
|
|
||||||
|
## Baseline
|
||||||
|
|
||||||
|
- Target framework: `net10.0`
|
||||||
|
- Plugin SDK: `LanMountainDesktop.PluginSdk` `5.0.0`
|
||||||
|
- Manifest: `plugin.json`
|
||||||
|
- Package: `.laapp`
|
||||||
|
- Runtime mode: `in-proc`
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -15,3 +23,19 @@ dotnet new lmd-plugin -n YourPluginName
|
|||||||
```
|
```
|
||||||
|
|
||||||
The generated project references `LanMountainDesktop.PluginSdk` and produces a `.laapp` package automatically when built.
|
The generated project references `LanMountainDesktop.PluginSdk` and produces a `.laapp` package automatically when built.
|
||||||
|
|
||||||
|
## Package contract
|
||||||
|
|
||||||
|
Every plugin package must contain:
|
||||||
|
|
||||||
|
- `plugin.json`
|
||||||
|
- the entrance assembly declared by `entranceAssembly`
|
||||||
|
- the `.deps.json` next to the entrance assembly
|
||||||
|
|
||||||
|
Optional package content:
|
||||||
|
|
||||||
|
- `Localization/*.json`
|
||||||
|
- plugin assets and other managed dependencies
|
||||||
|
- `airappmarket-entry.template.json` in the repository root for market publishing
|
||||||
|
|
||||||
|
Market publishing uses `market-manifest.json` with `schemaVersion`, `manifest`, `compatibility`, `repository`, `publication.packageSources`, and `capabilities`.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using LanMountainDesktop.Launcher.Services;
|
using LanMountainDesktop.Launcher.Services;
|
||||||
|
using System.IO.Compression;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Tests;
|
namespace LanMountainDesktop.Tests;
|
||||||
@@ -26,6 +27,96 @@ public sealed class PluginInstallerServiceTests : IDisposable
|
|||||||
Assert.Equal("plugin_elevation_required", result.Code);
|
Assert.Equal("plugin_elevation_required", result.Code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InstallPackage_InstallsLaappWithPluginJson_InsideUserScope()
|
||||||
|
{
|
||||||
|
var packagePath = Path.Combine(_tempRoot, "sample.laapp");
|
||||||
|
Directory.CreateDirectory(_tempRoot);
|
||||||
|
CreatePluginPackage(packagePath, "plugin.json", "plugin.install.sample", "Sample Plugin");
|
||||||
|
|
||||||
|
var pluginsDirectory = CreateUserScopedPluginsDirectory();
|
||||||
|
var service = new PluginInstallerService();
|
||||||
|
|
||||||
|
var result = service.InstallPackage(packagePath, pluginsDirectory);
|
||||||
|
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.Equal("ok", result.Code);
|
||||||
|
Assert.Equal("plugin.install.sample", result.ManifestId);
|
||||||
|
Assert.Equal("Sample Plugin", result.ManifestName);
|
||||||
|
Assert.NotNull(result.InstalledPackagePath);
|
||||||
|
Assert.True(File.Exists(result.InstalledPackagePath));
|
||||||
|
Assert.EndsWith(".laapp", result.InstalledPackagePath, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Empty(Directory.EnumerateFiles(pluginsDirectory, "*.incoming", SearchOption.AllDirectories));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InstallPackage_ReplacesExistingPackageWithSamePluginId()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_tempRoot);
|
||||||
|
var firstPackagePath = Path.Combine(_tempRoot, "sample-1.laapp");
|
||||||
|
var secondPackagePath = Path.Combine(_tempRoot, "sample-2.laapp");
|
||||||
|
CreatePluginPackage(firstPackagePath, "plugin.json", "plugin.replace.sample", "Sample Plugin v1");
|
||||||
|
CreatePluginPackage(secondPackagePath, "plugin.json", "plugin.replace.sample", "Sample Plugin v2");
|
||||||
|
|
||||||
|
var pluginsDirectory = CreateUserScopedPluginsDirectory();
|
||||||
|
var service = new PluginInstallerService();
|
||||||
|
|
||||||
|
var first = service.InstallPackage(firstPackagePath, pluginsDirectory);
|
||||||
|
var second = service.InstallPackage(secondPackagePath, pluginsDirectory);
|
||||||
|
|
||||||
|
Assert.True(first.Success);
|
||||||
|
Assert.True(second.Success);
|
||||||
|
Assert.Single(Directory.EnumerateFiles(pluginsDirectory, "*.laapp", SearchOption.TopDirectoryOnly));
|
||||||
|
Assert.True(File.Exists(second.InstalledPackagePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InstallPackage_StillSupportsLegacyManifestJson()
|
||||||
|
{
|
||||||
|
var packagePath = Path.Combine(_tempRoot, "legacy.lmdp");
|
||||||
|
Directory.CreateDirectory(_tempRoot);
|
||||||
|
CreatePluginPackage(packagePath, "manifest.json", "plugin.legacy.sample", "Legacy Plugin");
|
||||||
|
|
||||||
|
var pluginsDirectory = CreateUserScopedPluginsDirectory();
|
||||||
|
var service = new PluginInstallerService();
|
||||||
|
|
||||||
|
var result = service.InstallPackage(packagePath, pluginsDirectory);
|
||||||
|
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.Equal("plugin.legacy.sample", result.ManifestId);
|
||||||
|
Assert.True(File.Exists(result.InstalledPackagePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CreatePluginPackage(string packagePath, string manifestFileName, string pluginId, string pluginName)
|
||||||
|
{
|
||||||
|
using var archive = ZipFile.Open(packagePath, ZipArchiveMode.Create);
|
||||||
|
var entry = archive.CreateEntry(manifestFileName);
|
||||||
|
using var stream = entry.Open();
|
||||||
|
using var writer = new StreamWriter(stream);
|
||||||
|
writer.Write(
|
||||||
|
$$"""
|
||||||
|
{
|
||||||
|
"id": "{{pluginId}}",
|
||||||
|
"name": "{{pluginName}}",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateUserScopedPluginsDirectory()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"LanMountainDesktop",
|
||||||
|
"Tests",
|
||||||
|
nameof(PluginInstallerServiceTests),
|
||||||
|
Guid.NewGuid().ToString("N"),
|
||||||
|
"Extensions",
|
||||||
|
"Plugins");
|
||||||
|
Directory.CreateDirectory(root);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
127
LanMountainDesktop.Tests/PluginMarketIndexDocumentTests.cs
Normal file
127
LanMountainDesktop.Tests/PluginMarketIndexDocumentTests.cs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
using LanMountainDesktop.Services.PluginMarket;
|
||||||
|
using System.Net;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Tests;
|
||||||
|
|
||||||
|
public sealed class PluginMarketIndexDocumentTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Load_WithNestedV2Entry_MapsDisplayFieldsAndWorkspacePath()
|
||||||
|
{
|
||||||
|
var document = AirAppMarketIndexDocument.Load(CreateNestedIndexJson(), "test-index.json");
|
||||||
|
var plugin = Assert.Single(document.Plugins);
|
||||||
|
var source = Assert.Single(plugin.PackageSources);
|
||||||
|
|
||||||
|
Assert.Equal("LanMountainDesktop.SamplePlugin", plugin.Id);
|
||||||
|
Assert.Equal("LanMountain Sample Plugin", plugin.Name);
|
||||||
|
Assert.Equal("SDK v5 sample plugin.", plugin.Description);
|
||||||
|
Assert.Equal("LanMountainDesktop", plugin.Author);
|
||||||
|
Assert.Equal("0.4.0", plugin.Version);
|
||||||
|
Assert.Equal("5.0.0", plugin.ApiVersion);
|
||||||
|
Assert.Equal("0.0.1", plugin.MinHostVersion);
|
||||||
|
Assert.Equal("https://raw.githubusercontent.com/wwiinnddyy/LanAirApp/main/airappmarket/assets/sample-plugin.svg", plugin.IconUrl);
|
||||||
|
Assert.Equal("https://raw.githubusercontent.com/wwiinnddyy/LanMountainDesktop.SamplePlugin/main/README.md", plugin.ReadmeUrl);
|
||||||
|
Assert.Equal("workspace://LanMountainDesktop.SamplePlugin/LanMountainDesktop.SamplePlugin.0.4.0.laapp", source.Url);
|
||||||
|
Assert.Equal(PluginPackageSourceKind.WorkspaceLocal, source.SourceKind);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EnrichAsync_WhenRepositoryMetadataUnavailable_PreservesNestedDisplayFields()
|
||||||
|
{
|
||||||
|
var document = AirAppMarketIndexDocument.Load(
|
||||||
|
CreateNestedIndexJson("LanMountainDesktop.MissingPlugin"),
|
||||||
|
"test-index.json");
|
||||||
|
using var httpClient = new HttpClient(new NotFoundHandler());
|
||||||
|
using var resolver = new AirAppMarketMetadataResolverService(httpClient);
|
||||||
|
|
||||||
|
var enriched = await resolver.EnrichAsync(document);
|
||||||
|
var plugin = Assert.Single(enriched.Plugins);
|
||||||
|
|
||||||
|
Assert.Equal("LanMountain Sample Plugin", plugin.Name);
|
||||||
|
Assert.Equal("SDK v5 sample plugin.", plugin.Description);
|
||||||
|
Assert.Equal("LanMountainDesktop", plugin.Author);
|
||||||
|
Assert.Equal("0.4.0", plugin.Version);
|
||||||
|
Assert.Equal("5.0.0", plugin.ApiVersion);
|
||||||
|
Assert.Equal("0.0.1", plugin.MinHostVersion);
|
||||||
|
Assert.Equal("v0.4.0", plugin.ReleaseTag);
|
||||||
|
Assert.Equal("LanMountainDesktop.SamplePlugin.0.4.0.laapp", plugin.ReleaseAssetName);
|
||||||
|
Assert.Equal("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", plugin.Sha256);
|
||||||
|
Assert.Equal(1024, plugin.PackageSizeBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateNestedIndexJson(string repositoryName = "LanMountainDesktop.SamplePlugin")
|
||||||
|
{
|
||||||
|
return $$"""
|
||||||
|
{
|
||||||
|
"schemaVersion": "2.0.0",
|
||||||
|
"sourceId": "official",
|
||||||
|
"sourceName": "LanAirApp",
|
||||||
|
"generatedAt": "2026-04-29T00:00:00Z",
|
||||||
|
"contracts": [],
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"manifest": {
|
||||||
|
"id": "LanMountainDesktop.SamplePlugin",
|
||||||
|
"name": "LanMountain Sample Plugin",
|
||||||
|
"description": "SDK v5 sample plugin.",
|
||||||
|
"author": "LanMountainDesktop",
|
||||||
|
"version": "0.4.0",
|
||||||
|
"apiVersion": "5.0.0",
|
||||||
|
"entranceAssembly": "LanMountainDesktop.SamplePlugin.dll",
|
||||||
|
"sharedContracts": []
|
||||||
|
},
|
||||||
|
"compatibility": {
|
||||||
|
"minHostVersion": "0.0.1",
|
||||||
|
"apiVersion": "5.0.0"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"projectUrl": "https://github.com/wwiinnddyy/{{repositoryName}}",
|
||||||
|
"readmeUrl": "https://raw.githubusercontent.com/wwiinnddyy/{{repositoryName}}/main/README.md",
|
||||||
|
"homepageUrl": "https://github.com/wwiinnddyy/{{repositoryName}}",
|
||||||
|
"repositoryUrl": "https://github.com/wwiinnddyy/{{repositoryName}}",
|
||||||
|
"iconUrl": "https://raw.githubusercontent.com/wwiinnddyy/LanAirApp/main/airappmarket/assets/sample-plugin.svg",
|
||||||
|
"tags": [ "official", "sdk" ],
|
||||||
|
"releaseNotes": "Reference plugin for SDK v5 validation."
|
||||||
|
},
|
||||||
|
"publication": {
|
||||||
|
"releaseTag": "v0.4.0",
|
||||||
|
"releaseAssetName": "LanMountainDesktop.SamplePlugin.0.4.0.laapp",
|
||||||
|
"sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"packageSizeBytes": 1024,
|
||||||
|
"publishedAt": "2026-04-29T00:00:00Z",
|
||||||
|
"updatedAt": "2026-04-29T00:00:00Z",
|
||||||
|
"packageSources": [
|
||||||
|
{
|
||||||
|
"kind": "workspaceLocal",
|
||||||
|
"path": "workspace://LanMountainDesktop.SamplePlugin/LanMountainDesktop.SamplePlugin.0.4.0.laapp",
|
||||||
|
"assetName": "LanMountainDesktop.SamplePlugin.0.4.0.laapp",
|
||||||
|
"sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"sizeBytes": 1024,
|
||||||
|
"releaseTag": "v0.4.0",
|
||||||
|
"priority": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"desktopComponents": [ "LanMountainDesktop.SamplePlugin.StatusClock" ],
|
||||||
|
"settingsSections": [ "status" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NotFoundHandler : HttpMessageHandler
|
||||||
|
{
|
||||||
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
RequestMessage = request,
|
||||||
|
Content = new StringContent("{}")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using System.ComponentModel;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
@@ -111,7 +112,7 @@ internal sealed class LauncherClient
|
|||||||
WorkingDirectory = Path.GetDirectoryName(launcherPath) ?? AppContext.BaseDirectory,
|
WorkingDirectory = Path.GetDirectoryName(launcherPath) ?? AppContext.BaseDirectory,
|
||||||
Arguments = string.Create(
|
Arguments = string.Create(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
$"--source {QuoteArgument(Path.GetFullPath(packagePath))} --plugins-dir {QuoteArgument(Path.GetFullPath(pluginsDirectory))} --result {QuoteArgument(Path.GetFullPath(resultPath))} --launch-source plugin-install")
|
$"plugin install --source {QuoteArgument(Path.GetFullPath(packagePath))} --plugins-dir {QuoteArgument(Path.GetFullPath(pluginsDirectory))} --result {QuoteArgument(Path.GetFullPath(resultPath))} --launch-source plugin-install")
|
||||||
};
|
};
|
||||||
|
|
||||||
return Process.Start(startInfo);
|
return Process.Start(startInfo);
|
||||||
@@ -130,7 +131,17 @@ internal sealed class LauncherClient
|
|||||||
|
|
||||||
private static string ResolveLauncherPath()
|
private static string ResolveLauncherPath()
|
||||||
{
|
{
|
||||||
return Path.Combine(AppContext.BaseDirectory, "Launcher", LauncherExecutableName);
|
var baseDirectory = AppContext.BaseDirectory;
|
||||||
|
var candidates = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(baseDirectory, "Launcher", LauncherExecutableName),
|
||||||
|
Path.Combine(baseDirectory, LauncherExecutableName),
|
||||||
|
Path.GetFullPath(Path.Combine(baseDirectory, "..", "LanMountainDesktop.Launcher", LauncherExecutableName)),
|
||||||
|
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Debug", "net10.0", LauncherExecutableName)),
|
||||||
|
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Release", "net10.0", LauncherExecutableName))
|
||||||
|
};
|
||||||
|
|
||||||
|
return candidates.FirstOrDefault(File.Exists) ?? candidates[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string QuoteArgument(string value)
|
private static string QuoteArgument(string value)
|
||||||
|
|||||||
@@ -1236,7 +1236,31 @@ internal sealed class PluginCatalogSettingsService : IPluginCatalogSettingsServi
|
|||||||
repository,
|
repository,
|
||||||
publication,
|
publication,
|
||||||
sources,
|
sources,
|
||||||
[]);
|
BuildCapabilities(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<PluginCapabilityInfo> BuildCapabilities(AirAppMarketPluginEntry entry)
|
||||||
|
{
|
||||||
|
if (entry.Capabilities is null)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var capabilities = new List<PluginCapabilityInfo>();
|
||||||
|
capabilities.AddRange(entry.Capabilities.SharedContracts.Select(contract =>
|
||||||
|
new PluginCapabilityInfo(contract.Id, contract.Version, contract.AssemblyName)));
|
||||||
|
capabilities.AddRange(entry.Capabilities.DesktopComponents.Select(id =>
|
||||||
|
new PluginCapabilityInfo(id, null, null)));
|
||||||
|
capabilities.AddRange(entry.Capabilities.SettingsSections.Select(id =>
|
||||||
|
new PluginCapabilityInfo(id, null, null)));
|
||||||
|
capabilities.AddRange(entry.Capabilities.Exports.Select(id =>
|
||||||
|
new PluginCapabilityInfo(id, null, null)));
|
||||||
|
capabilities.AddRange(entry.Capabilities.MessageTypes.Select(id =>
|
||||||
|
new PluginCapabilityInfo(id, null, null)));
|
||||||
|
|
||||||
|
return capabilities
|
||||||
|
.DistinctBy(capability => $"{capability.Id}@{capability.Version}@{capability.AssemblyName}")
|
||||||
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<PluginPackageSourceInfo> BuildPackageSources(AirAppMarketPluginEntry entry)
|
private static IReadOnlyList<PluginPackageSourceInfo> BuildPackageSources(AirAppMarketPluginEntry entry)
|
||||||
|
|||||||
@@ -112,6 +112,9 @@ internal sealed class AirAppMarketMetadataResolverService : IDisposable
|
|||||||
? entry.PackageSources
|
? entry.PackageSources
|
||||||
: entry.Publication?.PackageSources ?? [];
|
: entry.Publication?.PackageSources ?? [];
|
||||||
var firstPackageSourceUrl = resolvedPackageSources.FirstOrDefault()?.Url ?? entry.DownloadUrl;
|
var firstPackageSourceUrl = resolvedPackageSources.FirstOrDefault()?.Url ?? entry.DownloadUrl;
|
||||||
|
var existingManifest = entry.Manifest;
|
||||||
|
var existingCompatibility = entry.Compatibility;
|
||||||
|
var existingPublication = entry.Publication;
|
||||||
|
|
||||||
return new AirAppMarketPluginEntry
|
return new AirAppMarketPluginEntry
|
||||||
{
|
{
|
||||||
@@ -142,9 +145,13 @@ internal sealed class AirAppMarketMetadataResolverService : IDisposable
|
|||||||
{
|
{
|
||||||
MinHostVersion = FirstNonEmpty(
|
MinHostVersion = FirstNonEmpty(
|
||||||
template?.MinHostVersion,
|
template?.MinHostVersion,
|
||||||
|
existingCompatibility?.MinHostVersion,
|
||||||
entry.MinHostVersion),
|
entry.MinHostVersion),
|
||||||
PluginApiVersion = FirstNonEmpty(
|
PluginApiVersion = FirstNonEmpty(
|
||||||
resolvedManifest?.ApiVersion,
|
resolvedManifest?.ApiVersion,
|
||||||
|
existingCompatibility?.PluginApiVersion,
|
||||||
|
existingCompatibility?.ApiVersion,
|
||||||
|
existingManifest?.ApiVersion,
|
||||||
entry.ApiVersion)
|
entry.ApiVersion)
|
||||||
?? string.Empty
|
?? string.Empty
|
||||||
}
|
}
|
||||||
@@ -162,19 +169,24 @@ internal sealed class AirAppMarketMetadataResolverService : IDisposable
|
|||||||
},
|
},
|
||||||
Publication = entry.Publication,
|
Publication = entry.Publication,
|
||||||
Capabilities = entry.Capabilities,
|
Capabilities = entry.Capabilities,
|
||||||
Id = FirstNonEmpty(resolvedManifest?.Id, entry.Id, entry.PluginId) ?? entry.PluginId,
|
Id = FirstNonEmpty(resolvedManifest?.Id, existingManifest?.Id, entry.Id, entry.PluginId) ?? entry.PluginId,
|
||||||
Name = FirstNonEmpty(resolvedManifest?.Name, entry.Name) ?? string.Empty,
|
Name = FirstNonEmpty(resolvedManifest?.Name, existingManifest?.Name, entry.Name) ?? string.Empty,
|
||||||
Description = FirstNonEmpty(resolvedManifest?.Description, entry.Description) ?? string.Empty,
|
Description = FirstNonEmpty(resolvedManifest?.Description, existingManifest?.Description, entry.Description) ?? string.Empty,
|
||||||
Author = FirstNonEmpty(resolvedManifest?.Author, entry.Author) ?? string.Empty,
|
Author = FirstNonEmpty(resolvedManifest?.Author, existingManifest?.Author, entry.Author) ?? string.Empty,
|
||||||
Version = FirstNonEmpty(resolvedManifest?.Version, entry.Version) ?? string.Empty,
|
Version = FirstNonEmpty(resolvedManifest?.Version, existingManifest?.Version, entry.Version) ?? string.Empty,
|
||||||
ApiVersion = FirstNonEmpty(resolvedManifest?.ApiVersion, entry.ApiVersion) ?? string.Empty,
|
ApiVersion = FirstNonEmpty(
|
||||||
MinHostVersion = FirstNonEmpty(template?.MinHostVersion, entry.MinHostVersion) ?? string.Empty,
|
resolvedManifest?.ApiVersion,
|
||||||
|
existingCompatibility?.PluginApiVersion,
|
||||||
|
existingCompatibility?.ApiVersion,
|
||||||
|
existingManifest?.ApiVersion,
|
||||||
|
entry.ApiVersion) ?? string.Empty,
|
||||||
|
MinHostVersion = FirstNonEmpty(template?.MinHostVersion, existingCompatibility?.MinHostVersion, entry.MinHostVersion) ?? string.Empty,
|
||||||
DownloadUrl = FirstNonEmpty(firstPackageSourceUrl, entry.DownloadUrl) ?? string.Empty,
|
DownloadUrl = FirstNonEmpty(firstPackageSourceUrl, entry.DownloadUrl) ?? string.Empty,
|
||||||
Sha256 = entry.Sha256,
|
Sha256 = FirstNonEmpty(existingPublication?.Sha256, entry.Sha256) ?? string.Empty,
|
||||||
PackageSizeBytes = entry.PackageSizeBytes,
|
PackageSizeBytes = existingPublication?.PackageSizeBytes > 0 ? existingPublication.PackageSizeBytes : entry.PackageSizeBytes,
|
||||||
IconUrl = FirstNonEmpty(template?.IconUrl, repository.IconUrl, entry.IconUrl) ?? string.Empty,
|
IconUrl = FirstNonEmpty(template?.IconUrl, repository.IconUrl, entry.IconUrl) ?? string.Empty,
|
||||||
ReleaseTag = entry.ReleaseTag,
|
ReleaseTag = FirstNonEmpty(existingPublication?.ReleaseTag, entry.ReleaseTag) ?? string.Empty,
|
||||||
ReleaseAssetName = entry.ReleaseAssetName,
|
ReleaseAssetName = FirstNonEmpty(existingPublication?.ReleaseAssetName, entry.ReleaseAssetName) ?? string.Empty,
|
||||||
ProjectUrl = FirstNonEmpty(template?.ProjectUrl, repository.ProjectUrl, entry.ProjectUrl) ?? string.Empty,
|
ProjectUrl = FirstNonEmpty(template?.ProjectUrl, repository.ProjectUrl, entry.ProjectUrl) ?? string.Empty,
|
||||||
ReadmeUrl = FirstNonEmpty(template?.ReadmeUrl, repository.ReadmeUrl, entry.ReadmeUrl) ?? string.Empty,
|
ReadmeUrl = FirstNonEmpty(template?.ReadmeUrl, repository.ReadmeUrl, entry.ReadmeUrl) ?? string.Empty,
|
||||||
HomepageUrl = FirstNonEmpty(template?.HomepageUrl, repository.HomepageUrl, entry.HomepageUrl) ?? string.Empty,
|
HomepageUrl = FirstNonEmpty(template?.HomepageUrl, repository.HomepageUrl, entry.HomepageUrl) ?? string.Empty,
|
||||||
@@ -191,9 +203,9 @@ internal sealed class AirAppMarketMetadataResolverService : IDisposable
|
|||||||
.ToList()
|
.ToList()
|
||||||
?? entry.SharedContracts,
|
?? entry.SharedContracts,
|
||||||
PackageSources = resolvedPackageSources,
|
PackageSources = resolvedPackageSources,
|
||||||
Md5 = entry.Md5,
|
Md5 = FirstNonEmpty(existingPublication?.Md5, entry.Md5) ?? string.Empty,
|
||||||
PublishedAt = entry.PublishedAt,
|
PublishedAt = existingPublication?.PublishedAt ?? entry.PublishedAt,
|
||||||
UpdatedAt = entry.UpdatedAt,
|
UpdatedAt = existingPublication?.UpdatedAt ?? entry.UpdatedAt,
|
||||||
ReleaseNotes = FirstNonEmpty(template?.ReleaseNotes, repository.ReleaseNotes, entry.ReleaseNotes) ?? string.Empty
|
ReleaseNotes = FirstNonEmpty(template?.ReleaseNotes, repository.ReleaseNotes, entry.ReleaseNotes) ?? string.Empty
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,7 +366,17 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
|||||||
|
|
||||||
private static string ResolveLauncherPath()
|
private static string ResolveLauncherPath()
|
||||||
{
|
{
|
||||||
return Path.Combine(AppContext.BaseDirectory, "Launcher", LauncherExecutableName);
|
var baseDirectory = AppContext.BaseDirectory;
|
||||||
|
var candidates = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(baseDirectory, "Launcher", LauncherExecutableName),
|
||||||
|
Path.Combine(baseDirectory, LauncherExecutableName),
|
||||||
|
Path.GetFullPath(Path.Combine(baseDirectory, "..", "LanMountainDesktop.Launcher", LauncherExecutableName)),
|
||||||
|
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Debug", "net10.0", LauncherExecutableName)),
|
||||||
|
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Release", "net10.0", LauncherExecutableName))
|
||||||
|
};
|
||||||
|
|
||||||
|
return candidates.FirstOrDefault(File.Exists) ?? candidates[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TryDeleteFile(string path)
|
private static void TryDeleteFile(string path)
|
||||||
|
|||||||
@@ -646,6 +646,8 @@ internal sealed class AirAppMarketPluginCompatibilityEntry
|
|||||||
{
|
{
|
||||||
public string MinHostVersion { get; init; } = string.Empty;
|
public string MinHostVersion { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string ApiVersion { get; init; } = string.Empty;
|
||||||
|
|
||||||
public string PluginApiVersion { get; init; } = string.Empty;
|
public string PluginApiVersion { get; init; } = string.Empty;
|
||||||
|
|
||||||
public AirAppMarketPluginCompatibilityEntry ValidateAndNormalize(string sourceName)
|
public AirAppMarketPluginCompatibilityEntry ValidateAndNormalize(string sourceName)
|
||||||
@@ -656,9 +658,13 @@ internal sealed class AirAppMarketPluginCompatibilityEntry
|
|||||||
MinHostVersion,
|
MinHostVersion,
|
||||||
nameof(MinHostVersion),
|
nameof(MinHostVersion),
|
||||||
sourceName),
|
sourceName),
|
||||||
|
ApiVersion = AirAppMarketIndexDocument.NormalizeVersion(
|
||||||
|
AirAppMarketIndexDocument.NormalizeValue(PluginApiVersion) ?? ApiVersion,
|
||||||
|
nameof(ApiVersion),
|
||||||
|
sourceName),
|
||||||
PluginApiVersion = AirAppMarketIndexDocument.NormalizeVersion(
|
PluginApiVersion = AirAppMarketIndexDocument.NormalizeVersion(
|
||||||
PluginApiVersion,
|
AirAppMarketIndexDocument.NormalizeValue(PluginApiVersion) ?? ApiVersion,
|
||||||
nameof(PluginApiVersion),
|
nameof(ApiVersion),
|
||||||
sourceName)
|
sourceName)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -742,6 +748,8 @@ internal sealed class AirAppMarketPluginPackageSourceEntry
|
|||||||
|
|
||||||
public string Url { get; init; } = string.Empty;
|
public string Url { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string Path { get; init; } = string.Empty;
|
||||||
|
|
||||||
public PluginPackageSourceKind SourceKind { get; init; } = PluginPackageSourceKind.ReleaseAsset;
|
public PluginPackageSourceKind SourceKind { get; init; } = PluginPackageSourceKind.ReleaseAsset;
|
||||||
|
|
||||||
public AirAppMarketPluginPackageSourceEntry ValidateAndNormalize(string sourceName, string pluginId)
|
public AirAppMarketPluginPackageSourceEntry ValidateAndNormalize(string sourceName, string pluginId)
|
||||||
@@ -755,9 +763,11 @@ internal sealed class AirAppMarketPluginPackageSourceEntry
|
|||||||
$"Market index '{sourceName}' declares invalid package source kind '{normalizedKind}' for plugin '{pluginId}'.");
|
$"Market index '{sourceName}' declares invalid package source kind '{normalizedKind}' for plugin '{pluginId}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var normalizedPath = AirAppMarketIndexDocument.NormalizeValue(Path);
|
||||||
var normalizedUrl = AirAppMarketIndexDocument.NormalizeValue(Url)
|
var normalizedUrl = AirAppMarketIndexDocument.NormalizeValue(Url)
|
||||||
|
?? normalizedPath
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
$"Market index '{sourceName}' is missing package source url for plugin '{pluginId}'.");
|
$"Market index '{sourceName}' is missing package source url/path for plugin '{pluginId}'.");
|
||||||
EnsurePackageSourceUrl(normalizedUrl, sourceName, pluginId);
|
EnsurePackageSourceUrl(normalizedUrl, sourceName, pluginId);
|
||||||
|
|
||||||
return new AirAppMarketPluginPackageSourceEntry
|
return new AirAppMarketPluginPackageSourceEntry
|
||||||
@@ -770,6 +780,7 @@ internal sealed class AirAppMarketPluginPackageSourceEntry
|
|||||||
_ => normalizedKind
|
_ => normalizedKind
|
||||||
},
|
},
|
||||||
Url = normalizedUrl,
|
Url = normalizedUrl,
|
||||||
|
Path = normalizedPath ?? string.Empty,
|
||||||
SourceKind = sourceKind
|
SourceKind = sourceKind
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1240,6 +1251,7 @@ internal sealed class AirAppMarketPluginEntry
|
|||||||
{
|
{
|
||||||
return compatibility is not null &&
|
return compatibility is not null &&
|
||||||
(!string.IsNullOrWhiteSpace(compatibility.MinHostVersion) ||
|
(!string.IsNullOrWhiteSpace(compatibility.MinHostVersion) ||
|
||||||
|
!string.IsNullOrWhiteSpace(compatibility.ApiVersion) ||
|
||||||
!string.IsNullOrWhiteSpace(compatibility.PluginApiVersion));
|
!string.IsNullOrWhiteSpace(compatibility.PluginApiVersion));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user