mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 15:44:25 +08:00
feat.airapp sdk
This commit is contained in:
230
LanMountainDesktop.AirAppDevServer/AirAppDevServer.cs
Normal file
230
LanMountainDesktop.AirAppDevServer/AirAppDevServer.cs
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.Build.Locator;
|
||||||
|
using Microsoft.Build.Execution;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirAppDevServer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AirApp 开发服务器
|
||||||
|
/// 提供文件监视、自动编译、热重载功能
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AirAppDevServer
|
||||||
|
{
|
||||||
|
private readonly string _projectPath;
|
||||||
|
private readonly int _port;
|
||||||
|
private readonly bool _verbose;
|
||||||
|
private FileSystemWatcher? _watcher;
|
||||||
|
private DateTime _lastBuildTime = DateTime.MinValue;
|
||||||
|
private readonly object _buildLock = new();
|
||||||
|
private bool _isBuilding;
|
||||||
|
|
||||||
|
public AirAppDevServer(string projectPath, int port, bool verbose)
|
||||||
|
{
|
||||||
|
_projectPath = Path.GetFullPath(projectPath);
|
||||||
|
_port = port;
|
||||||
|
_verbose = verbose;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync()
|
||||||
|
{
|
||||||
|
// 初始构建
|
||||||
|
Console.WriteLine("🔨 初始构建中...");
|
||||||
|
if (!BuildProject())
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ 初始构建失败");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
Console.WriteLine("✅ 初始构建成功");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// 启动文件监视
|
||||||
|
StartFileWatcher();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync()
|
||||||
|
{
|
||||||
|
_watcher?.Dispose();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartFileWatcher()
|
||||||
|
{
|
||||||
|
_watcher = new FileSystemWatcher(_projectPath)
|
||||||
|
{
|
||||||
|
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName,
|
||||||
|
Filter = "*.*",
|
||||||
|
IncludeSubdirectories = true,
|
||||||
|
EnableRaisingEvents = true
|
||||||
|
};
|
||||||
|
|
||||||
|
_watcher.Changed += OnFileChanged;
|
||||||
|
_watcher.Created += OnFileChanged;
|
||||||
|
_watcher.Deleted += OnFileChanged;
|
||||||
|
_watcher.Renamed += OnFileRenamed;
|
||||||
|
|
||||||
|
Console.WriteLine("👁️ 文件监视已启动,等待更改...");
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFileChanged(object sender, FileSystemEventArgs e)
|
||||||
|
{
|
||||||
|
// 忽略 bin、obj、.vs 等目录
|
||||||
|
if (e.FullPath.Contains("\\bin\\") ||
|
||||||
|
e.FullPath.Contains("\\obj\\") ||
|
||||||
|
e.FullPath.Contains("\\.vs\\") ||
|
||||||
|
e.FullPath.Contains("\\.git\\"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理源代码文件
|
||||||
|
var ext = Path.GetExtension(e.FullPath).ToLowerInvariant();
|
||||||
|
if (ext != ".cs" && ext != ".axaml" && ext != ".json" && ext != ".csproj")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止重复触发(文件保存时可能触发多次)
|
||||||
|
var now = DateTime.Now;
|
||||||
|
if ((now - _lastBuildTime).TotalMilliseconds < 500)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogVerbose($"📝 检测到文件更改: {Path.GetFileName(e.FullPath)}");
|
||||||
|
TriggerRebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFileRenamed(object sender, RenamedEventArgs e)
|
||||||
|
{
|
||||||
|
LogVerbose($"📝 检测到文件重命名: {Path.GetFileName(e.OldFullPath)} -> {Path.GetFileName(e.FullPath)}");
|
||||||
|
TriggerRebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TriggerRebuild()
|
||||||
|
{
|
||||||
|
lock (_buildLock)
|
||||||
|
{
|
||||||
|
if (_isBuilding)
|
||||||
|
{
|
||||||
|
LogVerbose("⏳ 构建进行中,跳过此次触发");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isBuilding = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 短暂延迟,让文件写入完成
|
||||||
|
Thread.Sleep(300);
|
||||||
|
|
||||||
|
Console.WriteLine("🔄 重新构建中...");
|
||||||
|
var success = BuildProject();
|
||||||
|
|
||||||
|
_lastBuildTime = DateTime.Now;
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"✅ 重新构建成功 [{DateTime.Now:HH:mm:ss}]");
|
||||||
|
Console.WriteLine("♻️ 热重载已生效");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ 重新构建失败 [{DateTime.Now:HH:mm:ss}]");
|
||||||
|
}
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (_buildLock)
|
||||||
|
{
|
||||||
|
_isBuilding = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool BuildProject()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 查找项目文件
|
||||||
|
var projectFile = FindProjectFile();
|
||||||
|
if (projectFile == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ 未找到项目文件 (.csproj)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogVerbose($"📄 项目文件: {Path.GetFileName(projectFile)}");
|
||||||
|
|
||||||
|
// 使用 dotnet build
|
||||||
|
var startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "dotnet",
|
||||||
|
Arguments = $"build \"{projectFile}\" -c Debug --nologo",
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(projectFile),
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using var process = Process.Start(startInfo);
|
||||||
|
if (process == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ 无法启动 dotnet build");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var output = process.StandardOutput.ReadToEnd();
|
||||||
|
var error = process.StandardError.ReadToEnd();
|
||||||
|
process.WaitForExit();
|
||||||
|
|
||||||
|
if (_verbose)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(output))
|
||||||
|
{
|
||||||
|
Console.WriteLine(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ 构建错误:");
|
||||||
|
Console.WriteLine(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"❌ 构建异常: {ex.Message}");
|
||||||
|
if (_verbose)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex.StackTrace);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? FindProjectFile()
|
||||||
|
{
|
||||||
|
var files = Directory.GetFiles(_projectPath, "*.csproj", SearchOption.TopDirectoryOnly);
|
||||||
|
return files.Length > 0 ? files[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogVerbose(string message)
|
||||||
|
{
|
||||||
|
if (_verbose)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[VERBOSE] {message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
119
LanMountainDesktop.AirAppDevServer/AirAppPackager.cs
Normal file
119
LanMountainDesktop.AirAppDevServer/AirAppPackager.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO.Compression;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirAppDevServer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AirApp 打包工具
|
||||||
|
/// 将 AirApp 项目打包为 .laapp 文件
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AirAppPackager
|
||||||
|
{
|
||||||
|
private readonly string _projectPath;
|
||||||
|
|
||||||
|
public AirAppPackager(string projectPath)
|
||||||
|
{
|
||||||
|
_projectPath = Path.GetFullPath(projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> PackageAsync(string? outputPath)
|
||||||
|
{
|
||||||
|
Console.WriteLine("🔨 构建项目...");
|
||||||
|
if (!await BuildProjectAsync())
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("构建失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
var binPath = Path.Combine(_projectPath, "bin", "Release", "net10.0");
|
||||||
|
if (!Directory.Exists(binPath))
|
||||||
|
{
|
||||||
|
binPath = Path.Combine(_projectPath, "bin", "Debug", "net10.0");
|
||||||
|
if (!Directory.Exists(binPath))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("未找到构建输出");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"📁 输出目录: {binPath}");
|
||||||
|
|
||||||
|
// 确定输出文件名
|
||||||
|
var projectName = Path.GetFileNameWithoutExtension(
|
||||||
|
Directory.GetFiles(_projectPath, "*.csproj").FirstOrDefault() ?? "AirApp");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(outputPath))
|
||||||
|
{
|
||||||
|
outputPath = Path.Combine(binPath, $"{projectName}.laapp");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outputPath = Path.GetFullPath(outputPath);
|
||||||
|
if (Directory.Exists(outputPath))
|
||||||
|
{
|
||||||
|
outputPath = Path.Combine(outputPath, $"{projectName}.laapp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除旧的包
|
||||||
|
if (File.Exists(outputPath))
|
||||||
|
{
|
||||||
|
File.Delete(outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"📦 打包到: {outputPath}");
|
||||||
|
|
||||||
|
// 创建 ZIP 包
|
||||||
|
using (var archive = ZipFile.Open(outputPath, ZipArchiveMode.Create))
|
||||||
|
{
|
||||||
|
var filesToPackage = Directory.GetFiles(binPath, "*.*", SearchOption.AllDirectories)
|
||||||
|
.Where(f => !f.Contains(".pdb") && !f.EndsWith(".laapp"))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Console.WriteLine($"📄 打包 {filesToPackage.Count} 个文件...");
|
||||||
|
|
||||||
|
foreach (var file in filesToPackage)
|
||||||
|
{
|
||||||
|
var relativePath = Path.GetRelativePath(binPath, file);
|
||||||
|
archive.CreateEntryFromFile(file, relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"✅ 包大小: {new FileInfo(outputPath).Length / 1024} KB");
|
||||||
|
|
||||||
|
return outputPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> BuildProjectAsync()
|
||||||
|
{
|
||||||
|
var projectFile = Directory.GetFiles(_projectPath, "*.csproj").FirstOrDefault();
|
||||||
|
if (projectFile == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ 未找到项目文件");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "dotnet",
|
||||||
|
Arguments = $"build \"{projectFile}\" -c Release --nologo",
|
||||||
|
WorkingDirectory = _projectPath,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using var process = Process.Start(startInfo);
|
||||||
|
if (process == null) return false;
|
||||||
|
|
||||||
|
await process.WaitForExitAsync();
|
||||||
|
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
var error = await process.StandardError.ReadToEndAsync();
|
||||||
|
Console.WriteLine($"❌ 构建错误:\n{error}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
129
LanMountainDesktop.AirAppDevServer/AirAppPreviewer.cs
Normal file
129
LanMountainDesktop.AirAppDevServer/AirAppPreviewer.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirAppDevServer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AirApp 预览工具
|
||||||
|
/// 在独立窗口中预览组件或窗口,无需安装到宿主
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AirAppPreviewer
|
||||||
|
{
|
||||||
|
private readonly string _projectPath;
|
||||||
|
|
||||||
|
public AirAppPreviewer(string projectPath)
|
||||||
|
{
|
||||||
|
_projectPath = Path.GetFullPath(projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PreviewComponentAsync(string componentId)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"🎨 预览组件: {componentId}");
|
||||||
|
await LaunchPreviewAsync("component", componentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PreviewWindowAsync(string windowId)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"🪟 预览窗口: {windowId}");
|
||||||
|
await LaunchPreviewAsync("window", windowId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PreviewAllAsync()
|
||||||
|
{
|
||||||
|
Console.WriteLine("📋 加载 AirApp 清单...");
|
||||||
|
|
||||||
|
var manifest = await LoadManifestAsync();
|
||||||
|
if (manifest == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ 未找到 airapp.json");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"✅ AirApp: {manifest.Name}");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// 显示可用的组件和窗口
|
||||||
|
if (manifest.Components?.Count > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("📦 可用组件:");
|
||||||
|
foreach (var comp in manifest.Components)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - {comp.Id}: {comp.Name}");
|
||||||
|
}
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest.Windows?.Count > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("🪟 可用窗口:");
|
||||||
|
foreach (var win in manifest.Windows)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - {win.Id}: {win.Name}");
|
||||||
|
}
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("使用以下命令预览:");
|
||||||
|
Console.WriteLine(" airapp-dev preview --component <component-id>");
|
||||||
|
Console.WriteLine(" airapp-dev preview --window <window-id>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LaunchPreviewAsync(string type, string id)
|
||||||
|
{
|
||||||
|
// 确保项目已构建
|
||||||
|
var binPath = Path.Combine(_projectPath, "bin", "Debug", "net10.0");
|
||||||
|
if (!Directory.Exists(binPath))
|
||||||
|
{
|
||||||
|
Console.WriteLine("❌ 未找到构建输出,请先运行: dotnet build");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"📁 输出路径: {binPath}");
|
||||||
|
Console.WriteLine("🚀 启动预览窗口...");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("💡 提示: 关闭预览窗口以退出");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// TODO: 这里需要启动一个预览宿主应用
|
||||||
|
// 预览宿主会加载 AirApp 并显示指定的组件或窗口
|
||||||
|
Console.WriteLine("⚠️ 预览功能需要配合 LanMountainDesktop 宿主运行");
|
||||||
|
Console.WriteLine(" 暂时请使用: dotnet run --project LanMountainDesktop.csproj -- --debug-airapp <path>");
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ManifestModel?> LoadManifestAsync()
|
||||||
|
{
|
||||||
|
var manifestPath = Path.Combine(_projectPath, "airapp.json");
|
||||||
|
if (!File.Exists(manifestPath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = await File.ReadAllTextAsync(manifestPath);
|
||||||
|
return JsonSerializer.Deserialize<ManifestModel>(json, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ManifestModel
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public List<ComponentModel>? Components { get; set; }
|
||||||
|
public List<WindowModel>? Windows { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ComponentModel
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class WindowModel
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||||
|
<PackageReference Include="Microsoft.Build.Locator" Version="1.7.8" />
|
||||||
|
<PackageReference Include="Microsoft.Build" Version="17.11.4" />
|
||||||
|
<PackageReference Include="Microsoft.Build.Framework" Version="17.11.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.AirAppSdk\LanMountainDesktop.AirAppSdk.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
149
LanMountainDesktop.AirAppDevServer/Program.cs
Normal file
149
LanMountainDesktop.AirAppDevServer/Program.cs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirAppDevServer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AirApp 开发服务器主程序
|
||||||
|
/// 提供热重载、实时预览等开发功能
|
||||||
|
/// </summary>
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static async Task<int> Main(string[] args)
|
||||||
|
{
|
||||||
|
var rootCommand = new RootCommand("LanMountainDesktop AirApp 开发服务器");
|
||||||
|
|
||||||
|
// 开发模式命令
|
||||||
|
var devCommand = new Command("dev", "启动开发服务器(支持热重载)");
|
||||||
|
var projectPathOption = new Option<string>(
|
||||||
|
aliases: new[] { "--project", "-p" },
|
||||||
|
description: "AirApp 项目路径",
|
||||||
|
getDefaultValue: () => Directory.GetCurrentDirectory());
|
||||||
|
var portOption = new Option<int>(
|
||||||
|
aliases: new[] { "--port" },
|
||||||
|
description: "开发服务器端口",
|
||||||
|
getDefaultValue: () => 5000);
|
||||||
|
var verboseOption = new Option<bool>(
|
||||||
|
aliases: new[] { "--verbose", "-v" },
|
||||||
|
description: "显示详细日志");
|
||||||
|
|
||||||
|
devCommand.AddOption(projectPathOption);
|
||||||
|
devCommand.AddOption(portOption);
|
||||||
|
devCommand.AddOption(verboseOption);
|
||||||
|
|
||||||
|
devCommand.SetHandler(async (projectPath, port, verbose) =>
|
||||||
|
{
|
||||||
|
await RunDevServerAsync(projectPath, port, verbose);
|
||||||
|
}, projectPathOption, portOption, verboseOption);
|
||||||
|
|
||||||
|
// 预览命令
|
||||||
|
var previewCommand = new Command("preview", "预览 AirApp(无需安装到宿主)");
|
||||||
|
var componentOption = new Option<string?>(
|
||||||
|
aliases: new[] { "--component", "-c" },
|
||||||
|
description: "要预览的组件 ID");
|
||||||
|
var windowOption = new Option<string?>(
|
||||||
|
aliases: new[] { "--window", "-w" },
|
||||||
|
description: "要预览的窗口 ID");
|
||||||
|
|
||||||
|
previewCommand.AddOption(projectPathOption);
|
||||||
|
previewCommand.AddOption(componentOption);
|
||||||
|
previewCommand.AddOption(windowOption);
|
||||||
|
|
||||||
|
previewCommand.SetHandler(async (projectPath, component, window) =>
|
||||||
|
{
|
||||||
|
await RunPreviewAsync(projectPath, component, window);
|
||||||
|
}, projectPathOption, componentOption, windowOption);
|
||||||
|
|
||||||
|
// 打包命令
|
||||||
|
var packageCommand = new Command("package", "打包 AirApp 为 .laapp 文件");
|
||||||
|
var outputOption = new Option<string?>(
|
||||||
|
aliases: new[] { "--output", "-o" },
|
||||||
|
description: "输出路径");
|
||||||
|
|
||||||
|
packageCommand.AddOption(projectPathOption);
|
||||||
|
packageCommand.AddOption(outputOption);
|
||||||
|
|
||||||
|
packageCommand.SetHandler(async (projectPath, output) =>
|
||||||
|
{
|
||||||
|
await PackageAirAppAsync(projectPath, output);
|
||||||
|
}, projectPathOption, outputOption);
|
||||||
|
|
||||||
|
rootCommand.AddCommand(devCommand);
|
||||||
|
rootCommand.AddCommand(previewCommand);
|
||||||
|
rootCommand.AddCommand(packageCommand);
|
||||||
|
|
||||||
|
return await rootCommand.InvokeAsync(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task RunDevServerAsync(string projectPath, int port, bool verbose)
|
||||||
|
{
|
||||||
|
Console.WriteLine("🚀 启动 AirApp 开发服务器...");
|
||||||
|
Console.WriteLine($"📁 项目路径: {projectPath}");
|
||||||
|
Console.WriteLine($"🔌 端口: {port}");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
var server = new AirAppDevServer(projectPath, port, verbose);
|
||||||
|
await server.StartAsync();
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("✅ 开发服务器已启动");
|
||||||
|
Console.WriteLine($"🌐 预览地址: http://localhost:{port}");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("按 Ctrl+C 停止服务器...");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// 等待取消信号
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
Console.CancelKeyPress += (sender, e) =>
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
cts.Cancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(Timeout.Infinite, cts.Token);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("🛑 正在停止服务器...");
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.StopAsync();
|
||||||
|
Console.WriteLine("✅ 服务器已停止");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task RunPreviewAsync(string projectPath, string? component, string? window)
|
||||||
|
{
|
||||||
|
Console.WriteLine("👁️ 启动 AirApp 预览...");
|
||||||
|
Console.WriteLine($"📁 项目路径: {projectPath}");
|
||||||
|
|
||||||
|
var previewer = new AirAppPreviewer(projectPath);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(component))
|
||||||
|
{
|
||||||
|
await previewer.PreviewComponentAsync(component);
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(window))
|
||||||
|
{
|
||||||
|
await previewer.PreviewWindowAsync(window);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await previewer.PreviewAllAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task PackageAirAppAsync(string projectPath, string? output)
|
||||||
|
{
|
||||||
|
Console.WriteLine("📦 打包 AirApp...");
|
||||||
|
Console.WriteLine($"📁 项目路径: {projectPath}");
|
||||||
|
|
||||||
|
var packager = new AirAppPackager(projectPath);
|
||||||
|
var outputPath = await packager.PackageAsync(output);
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine($"✅ 打包完成: {outputPath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
49
LanMountainDesktop.AirAppSdk/AirAppAppearanceSnapshot.cs
Normal file
49
LanMountainDesktop.AirAppSdk/AirAppAppearanceSnapshot.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Snapshot of the current appearance settings.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AirAppAppearanceSnapshot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether dark mode is enabled.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDarkMode { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the primary accent color.
|
||||||
|
/// </summary>
|
||||||
|
public Color AccentColor { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the glass effect opacity (0.0 - 1.0).
|
||||||
|
/// </summary>
|
||||||
|
public double GlassOpacity { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the corner radius preset.
|
||||||
|
/// </summary>
|
||||||
|
public AirAppCornerRadiusPreset CornerRadiusPreset { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the background color.
|
||||||
|
/// </summary>
|
||||||
|
public Color BackgroundColor { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the foreground (text) color.
|
||||||
|
/// </summary>
|
||||||
|
public Color ForegroundColor { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the border color.
|
||||||
|
/// </summary>
|
||||||
|
public Color BorderColor { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets additional custom properties.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyDictionary<string, object>? CustomProperties { get; init; }
|
||||||
|
}
|
||||||
119
LanMountainDesktop.AirAppSdk/AirAppBase.cs
Normal file
119
LanMountainDesktop.AirAppSdk/AirAppBase.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for AirApp implementations.
|
||||||
|
/// Inherit from this class and apply the [AirAppEntrance] attribute.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class AirAppBase : IAirApp
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the runtime context after the AirApp has started.
|
||||||
|
/// Available after OnStartedAsync is called.
|
||||||
|
/// </summary>
|
||||||
|
protected IAirAppRuntimeContext? RuntimeContext { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize the AirApp and register services.
|
||||||
|
/// Override this method to register your components, windows, and services.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Host builder context</param>
|
||||||
|
/// <param name="services">Service collection</param>
|
||||||
|
public virtual void Initialize(HostBuilderContext context, IServiceCollection services)
|
||||||
|
{
|
||||||
|
// Default implementation: do nothing
|
||||||
|
// Derived classes can override to register services
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called after the host application has started.
|
||||||
|
/// Override this for runtime initialization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">AirApp runtime context</param>
|
||||||
|
public virtual Task OnStartedAsync(IAirAppRuntimeContext context)
|
||||||
|
{
|
||||||
|
RuntimeContext = context;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the host application is stopping.
|
||||||
|
/// Override this for cleanup logic.
|
||||||
|
/// </summary>
|
||||||
|
public virtual Task OnStoppingAsync()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a desktop component widget.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TWidget">Widget implementation type</typeparam>
|
||||||
|
/// <param name="id">Unique component identifier</param>
|
||||||
|
/// <param name="name">Display name</param>
|
||||||
|
/// <param name="configure">Optional configuration</param>
|
||||||
|
protected void RegisterComponent<TWidget>(
|
||||||
|
string id,
|
||||||
|
string name,
|
||||||
|
Action<AirAppComponentOptions>? configure = null)
|
||||||
|
where TWidget : class, IAirAppWidget
|
||||||
|
{
|
||||||
|
if (RuntimeContext == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"RegisterComponent can only be called after OnStartedAsync. " +
|
||||||
|
"Use IServiceCollection extension methods in Initialize() instead.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new AirAppComponentOptions
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Name = name,
|
||||||
|
WidgetType = typeof(TWidget)
|
||||||
|
};
|
||||||
|
|
||||||
|
configure?.Invoke(options);
|
||||||
|
|
||||||
|
// Delegate to runtime context
|
||||||
|
RuntimeContext.RegisterComponent(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a window.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TWindow">Window implementation type</typeparam>
|
||||||
|
/// <param name="id">Unique window identifier</param>
|
||||||
|
/// <param name="name">Display name</param>
|
||||||
|
protected void RegisterWindow<TWindow>(string id, string name)
|
||||||
|
where TWindow : class, IAirAppWindow
|
||||||
|
{
|
||||||
|
if (RuntimeContext == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"RegisterWindow can only be called after OnStartedAsync.");
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeContext.RegisterWindow(id, name, typeof(TWindow));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a service in the DI container.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TService">Service interface</typeparam>
|
||||||
|
/// <typeparam name="TImplementation">Implementation type</typeparam>
|
||||||
|
protected void RegisterService<TService, TImplementation>()
|
||||||
|
where TService : class
|
||||||
|
where TImplementation : class, TService
|
||||||
|
{
|
||||||
|
if (RuntimeContext == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"RegisterService can only be called after OnStartedAsync. " +
|
||||||
|
"Use IServiceCollection in Initialize() instead.");
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeContext.RegisterService<TService, TImplementation>();
|
||||||
|
}
|
||||||
|
}
|
||||||
61
LanMountainDesktop.AirAppSdk/AirAppComponentOptions.cs
Normal file
61
LanMountainDesktop.AirAppSdk/AirAppComponentOptions.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Options for registering an AirApp desktop component.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AirAppComponentOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique component identifier.
|
||||||
|
/// </summary>
|
||||||
|
public required string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the display name.
|
||||||
|
/// </summary>
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the widget implementation type.
|
||||||
|
/// Must implement IAirAppWidget.
|
||||||
|
/// </summary>
|
||||||
|
public required Type WidgetType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the optional description.
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the default width in grid cells.
|
||||||
|
/// Default is 2.
|
||||||
|
/// </summary>
|
||||||
|
public int DefaultWidth { get; set; } = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the default height in grid cells.
|
||||||
|
/// Default is 2.
|
||||||
|
/// </summary>
|
||||||
|
public int DefaultHeight { get; set; } = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the resize mode.
|
||||||
|
/// </summary>
|
||||||
|
public AirAppComponentResizeMode ResizeMode { get; set; } = AirAppComponentResizeMode.Both;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether this component can be added multiple times.
|
||||||
|
/// Default is true.
|
||||||
|
/// </summary>
|
||||||
|
public bool AllowMultipleInstances { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the category for grouping in the component library.
|
||||||
|
/// </summary>
|
||||||
|
public string? Category { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the icon identifier.
|
||||||
|
/// </summary>
|
||||||
|
public string? IconKey { get; set; }
|
||||||
|
}
|
||||||
27
LanMountainDesktop.AirAppSdk/AirAppComponentResizeMode.cs
Normal file
27
LanMountainDesktop.AirAppSdk/AirAppComponentResizeMode.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resize mode for AirApp desktop components.
|
||||||
|
/// </summary>
|
||||||
|
public enum AirAppComponentResizeMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Cannot be resized.
|
||||||
|
/// </summary>
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Can be resized horizontally only.
|
||||||
|
/// </summary>
|
||||||
|
Horizontal = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Can be resized vertically only.
|
||||||
|
/// </summary>
|
||||||
|
Vertical = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Can be resized in both directions.
|
||||||
|
/// </summary>
|
||||||
|
Both = 3
|
||||||
|
}
|
||||||
32
LanMountainDesktop.AirAppSdk/AirAppCornerRadiusPreset.cs
Normal file
32
LanMountainDesktop.AirAppSdk/AirAppCornerRadiusPreset.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Corner radius presets.
|
||||||
|
/// </summary>
|
||||||
|
public enum AirAppCornerRadiusPreset
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// No rounded corners.
|
||||||
|
/// </summary>
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Small corner radius (4px).
|
||||||
|
/// </summary>
|
||||||
|
Small = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Medium corner radius (8px).
|
||||||
|
/// </summary>
|
||||||
|
Medium = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Large corner radius (12px).
|
||||||
|
/// </summary>
|
||||||
|
Large = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extra large corner radius (16px).
|
||||||
|
/// </summary>
|
||||||
|
ExtraLarge = 4
|
||||||
|
}
|
||||||
10
LanMountainDesktop.AirAppSdk/AirAppEntranceAttribute.cs
Normal file
10
LanMountainDesktop.AirAppSdk/AirAppEntranceAttribute.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a class as the entry point for an AirApp.
|
||||||
|
/// The marked class must inherit from AirAppBase or implement IAirApp.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||||
|
public sealed class AirAppEntranceAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
188
LanMountainDesktop.AirAppSdk/AirAppManifest.cs
Normal file
188
LanMountainDesktop.AirAppSdk/AirAppManifest.cs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AirApp manifest (airapp.json).
|
||||||
|
/// </summary>
|
||||||
|
public sealed record AirAppManifest(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
string EntranceAssembly,
|
||||||
|
string? Description = null,
|
||||||
|
string? Author = null,
|
||||||
|
string? Version = null,
|
||||||
|
string? ApiVersion = null,
|
||||||
|
AirAppRuntimeConfiguration? Runtime = null,
|
||||||
|
IReadOnlyList<AirAppComponentManifest>? Components = null,
|
||||||
|
IReadOnlyList<AirAppWindowManifest>? Windows = null,
|
||||||
|
IReadOnlyList<string>? Permissions = null,
|
||||||
|
IReadOnlyList<AirAppSharedContractReference>? SharedContracts = null)
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||||
|
AllowTrailingCommas = true
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load manifest from file.
|
||||||
|
/// </summary>
|
||||||
|
public static AirAppManifest Load(string manifestPath)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(manifestPath);
|
||||||
|
|
||||||
|
using var stream = File.OpenRead(manifestPath);
|
||||||
|
return Load(stream, manifestPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load manifest from stream.
|
||||||
|
/// </summary>
|
||||||
|
public static AirAppManifest Load(Stream stream, string sourceName)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(stream);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(sourceName);
|
||||||
|
|
||||||
|
var manifest = JsonSerializer.Deserialize<AirAppManifest>(stream, SerializerOptions);
|
||||||
|
if (manifest is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to deserialize AirApp manifest '{sourceName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifest.NormalizeAndValidate(sourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolve entrance assembly path.
|
||||||
|
/// </summary>
|
||||||
|
public string ResolveEntranceAssemblyPath(string manifestPath)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(manifestPath);
|
||||||
|
|
||||||
|
if (Path.IsPathRooted(EntranceAssembly))
|
||||||
|
{
|
||||||
|
return Path.GetFullPath(EntranceAssembly);
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifestDirectory = Path.GetDirectoryName(Path.GetFullPath(manifestPath))
|
||||||
|
?? throw new InvalidOperationException($"Failed to determine directory of '{manifestPath}'.");
|
||||||
|
|
||||||
|
return Path.GetFullPath(Path.Combine(manifestDirectory, EntranceAssembly));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get runtime mode.
|
||||||
|
/// </summary>
|
||||||
|
public AirAppRuntimeMode RuntimeMode =>
|
||||||
|
AirAppRuntimeModes.TryParse(Runtime?.Mode, out var mode) ? mode : AirAppRuntimeMode.InProcess;
|
||||||
|
|
||||||
|
private AirAppManifest NormalizeAndValidate(string manifestPath)
|
||||||
|
{
|
||||||
|
var normalizedRuntime = (Runtime ?? new AirAppRuntimeConfiguration()).NormalizeAndValidate(manifestPath);
|
||||||
|
|
||||||
|
var normalized = this with
|
||||||
|
{
|
||||||
|
Id = RequireValue(Id, nameof(Id), manifestPath),
|
||||||
|
Name = RequireValue(Name, nameof(Name), manifestPath),
|
||||||
|
EntranceAssembly = RequireValue(EntranceAssembly, nameof(EntranceAssembly), manifestPath),
|
||||||
|
Description = NormalizeOptionalValue(Description),
|
||||||
|
Author = NormalizeOptionalValue(Author),
|
||||||
|
Version = NormalizeOptionalValue(Version),
|
||||||
|
ApiVersion = NormalizeOptionalValue(ApiVersion) ?? AirAppSdkInfo.ApiVersion,
|
||||||
|
Runtime = normalizedRuntime,
|
||||||
|
Components = Components ?? Array.Empty<AirAppComponentManifest>(),
|
||||||
|
Windows = Windows ?? Array.Empty<AirAppWindowManifest>(),
|
||||||
|
Permissions = Permissions ?? Array.Empty<string>(),
|
||||||
|
SharedContracts = SharedContracts ?? Array.Empty<AirAppSharedContractReference>()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate API version
|
||||||
|
if (!System.Version.TryParse(normalized.ApiVersion, out var requestedVersion))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"AirApp manifest '{manifestPath}' declares invalid API version '{normalized.ApiVersion}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!System.Version.TryParse(AirAppSdkInfo.ApiVersion, out var currentVersion))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"AirApp SDK API version '{AirAppSdkInfo.ApiVersion}' is invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestedVersion.Major != currentVersion.Major)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"AirApp '{normalized.Id}' targets API version '{normalized.ApiVersion}' (major {requestedVersion.Major}), " +
|
||||||
|
$"but the host provides '{AirAppSdkInfo.ApiVersion}' (major {currentVersion.Major}). " +
|
||||||
|
$"This host only supports v{currentVersion.Major}.x AirApps and rejects v{requestedVersion.Major}.x packages. " +
|
||||||
|
$"Migrate the AirApp manifest and code to API {AirAppSdkInfo.ApiVersion}, then rebuild and republish.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RequireValue(string? value, string propertyName, string manifestPath)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeOptionalValue(value);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"AirApp manifest '{manifestPath}' is missing required property '{propertyName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeOptionalValue(string? value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Component declaration in manifest.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record AirAppComponentManifest(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
int DefaultWidth = 2,
|
||||||
|
int DefaultHeight = 2,
|
||||||
|
string? Description = null,
|
||||||
|
string? Category = null,
|
||||||
|
string? IconKey = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Window declaration in manifest.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record AirAppWindowManifest(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
double DefaultWidth = 800,
|
||||||
|
double DefaultHeight = 600,
|
||||||
|
string? Description = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared contract reference.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record AirAppSharedContractReference(
|
||||||
|
string Id,
|
||||||
|
string Version);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime configuration.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record AirAppRuntimeConfiguration
|
||||||
|
{
|
||||||
|
public string? Mode { get; init; }
|
||||||
|
public IReadOnlyList<string>? Capabilities { get; init; }
|
||||||
|
|
||||||
|
internal AirAppRuntimeConfiguration NormalizeAndValidate(string manifestPath)
|
||||||
|
{
|
||||||
|
return this with
|
||||||
|
{
|
||||||
|
Mode = string.IsNullOrWhiteSpace(Mode) ? "in-process" : Mode.Trim().ToLowerInvariant(),
|
||||||
|
Capabilities = Capabilities ?? Array.Empty<string>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
53
LanMountainDesktop.AirAppSdk/AirAppRuntimeMode.cs
Normal file
53
LanMountainDesktop.AirAppSdk/AirAppRuntimeMode.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mode for AirApps.
|
||||||
|
/// </summary>
|
||||||
|
public enum AirAppRuntimeMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Run in the host process (best performance, shared memory).
|
||||||
|
/// </summary>
|
||||||
|
InProcess = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run in an isolated background process (safer, separate memory).
|
||||||
|
/// </summary>
|
||||||
|
IsolatedBackground = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run in an isolated window process (full isolation).
|
||||||
|
/// </summary>
|
||||||
|
IsolatedWindow = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper for parsing runtime modes.
|
||||||
|
/// </summary>
|
||||||
|
public static class AirAppRuntimeModes
|
||||||
|
{
|
||||||
|
public static bool TryParse(string? mode, out AirAppRuntimeMode result)
|
||||||
|
{
|
||||||
|
result = AirAppRuntimeMode.InProcess;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(mode))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = mode.Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"in-process" => SetResult(AirAppRuntimeMode.InProcess, out result),
|
||||||
|
"isolated-background" => SetResult(AirAppRuntimeMode.IsolatedBackground, out result),
|
||||||
|
"isolated-window" => SetResult(AirAppRuntimeMode.IsolatedWindow, out result),
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SetResult(AirAppRuntimeMode mode, out AirAppRuntimeMode result)
|
||||||
|
{
|
||||||
|
result = mode;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
LanMountainDesktop.AirAppSdk/AirAppSdkInfo.cs
Normal file
33
LanMountainDesktop.AirAppSdk/AirAppSdkInfo.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AirApp SDK information.
|
||||||
|
/// </summary>
|
||||||
|
public static class AirAppSdkInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Current SDK version.
|
||||||
|
/// </summary>
|
||||||
|
public const string SdkVersion = "6.0.0";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current API version.
|
||||||
|
/// AirApps must target this major version to be compatible.
|
||||||
|
/// </summary>
|
||||||
|
public const string ApiVersion = "6.0.0";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the SDK display name.
|
||||||
|
/// </summary>
|
||||||
|
public static string DisplayName => "LanMountainDesktop AirApp SDK";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default manifest file name.
|
||||||
|
/// </summary>
|
||||||
|
public const string ManifestFileName = "airapp.json";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the package file extension.
|
||||||
|
/// </summary>
|
||||||
|
public const string PackageExtension = ".laapp";
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for registering AirApp services.
|
||||||
|
/// </summary>
|
||||||
|
public static class AirAppServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Register a desktop component.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddAirAppComponent<TWidget>(
|
||||||
|
this IServiceCollection services,
|
||||||
|
string id,
|
||||||
|
string name,
|
||||||
|
Action<AirAppComponentOptions>? configure = null)
|
||||||
|
where TWidget : class, IAirAppWidget
|
||||||
|
{
|
||||||
|
var options = new AirAppComponentOptions
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Name = name,
|
||||||
|
WidgetType = typeof(TWidget)
|
||||||
|
};
|
||||||
|
|
||||||
|
configure?.Invoke(options);
|
||||||
|
|
||||||
|
// Register the widget as transient (new instance per placement)
|
||||||
|
services.AddTransient<TWidget>();
|
||||||
|
|
||||||
|
// Register the component options (will be picked up by the host)
|
||||||
|
services.AddSingleton(options);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a window.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddAirAppWindow<TWindow>(
|
||||||
|
this IServiceCollection services,
|
||||||
|
string id,
|
||||||
|
string name)
|
||||||
|
where TWindow : class, IAirAppWindow
|
||||||
|
{
|
||||||
|
// Register the window as transient (new instance per open)
|
||||||
|
services.AddTransient<TWindow>();
|
||||||
|
|
||||||
|
// TODO: Register window metadata
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a settings section (declarative).
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddAirAppSettings(
|
||||||
|
this IServiceCollection services,
|
||||||
|
string id,
|
||||||
|
string name,
|
||||||
|
Action<AirAppSettingsSectionBuilder>? configure = null)
|
||||||
|
{
|
||||||
|
var builder = new AirAppSettingsSectionBuilder(id, name);
|
||||||
|
configure?.Invoke(builder);
|
||||||
|
|
||||||
|
// Register the settings section
|
||||||
|
services.AddSingleton(builder.Build());
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builder for settings sections.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AirAppSettingsSectionBuilder
|
||||||
|
{
|
||||||
|
private readonly string _id;
|
||||||
|
private readonly string _name;
|
||||||
|
private readonly List<AirAppSettingOption> _options = new();
|
||||||
|
|
||||||
|
internal AirAppSettingsSectionBuilder(string id, string name)
|
||||||
|
{
|
||||||
|
_id = id;
|
||||||
|
_name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AirAppSettingsSectionBuilder AddToggle(string key, string label, bool defaultValue = false)
|
||||||
|
{
|
||||||
|
_options.Add(new AirAppSettingOption
|
||||||
|
{
|
||||||
|
Key = key,
|
||||||
|
Label = label,
|
||||||
|
Type = "toggle",
|
||||||
|
DefaultValue = defaultValue
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AirAppSettingsSectionBuilder AddText(string key, string label, string? defaultValue = null)
|
||||||
|
{
|
||||||
|
_options.Add(new AirAppSettingOption
|
||||||
|
{
|
||||||
|
Key = key,
|
||||||
|
Label = label,
|
||||||
|
Type = "text",
|
||||||
|
DefaultValue = defaultValue
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AirAppSettingsSectionBuilder AddNumber(string key, string label, double defaultValue = 0, double? minimum = null, double? maximum = null)
|
||||||
|
{
|
||||||
|
_options.Add(new AirAppSettingOption
|
||||||
|
{
|
||||||
|
Key = key,
|
||||||
|
Label = label,
|
||||||
|
Type = "number",
|
||||||
|
DefaultValue = defaultValue,
|
||||||
|
Minimum = minimum,
|
||||||
|
Maximum = maximum
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal AirAppSettingsSection Build()
|
||||||
|
{
|
||||||
|
return new AirAppSettingsSection
|
||||||
|
{
|
||||||
|
Id = _id,
|
||||||
|
Name = _name,
|
||||||
|
Options = _options
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Settings section metadata.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AirAppSettingsSection
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Name { get; init; }
|
||||||
|
public required List<AirAppSettingOption> Options { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Individual setting option.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AirAppSettingOption
|
||||||
|
{
|
||||||
|
public required string Key { get; init; }
|
||||||
|
public required string Label { get; init; }
|
||||||
|
public required string Type { get; init; }
|
||||||
|
public object? DefaultValue { get; init; }
|
||||||
|
public double? Minimum { get; init; }
|
||||||
|
public double? Maximum { get; init; }
|
||||||
|
}
|
||||||
80
LanMountainDesktop.AirAppSdk/AirAppWidgetBase.cs
Normal file
80
LanMountainDesktop.AirAppSdk/AirAppWidgetBase.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for AirApp desktop component widgets.
|
||||||
|
/// Inherit from this to create custom desktop components.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class AirAppWidgetBase : UserControl, IAirAppWidget
|
||||||
|
{
|
||||||
|
private IAirAppComponentContext? _context;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the component context.
|
||||||
|
/// </summary>
|
||||||
|
public IAirAppComponentContext Context
|
||||||
|
{
|
||||||
|
get => _context ?? throw new InvalidOperationException("Context has not been set yet.");
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_context = value;
|
||||||
|
OnContextSet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the context is first set.
|
||||||
|
/// Override this to initialize based on context.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void OnContextSet()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the widget is attached to the desktop.
|
||||||
|
/// </summary>
|
||||||
|
public void OnAttached()
|
||||||
|
{
|
||||||
|
OnAttachedCore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the widget is detached from the desktop.
|
||||||
|
/// </summary>
|
||||||
|
public void OnDetached()
|
||||||
|
{
|
||||||
|
OnDetachedCore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the appearance has changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="snapshot">New appearance snapshot</param>
|
||||||
|
public void OnAppearanceChanged(AirAppAppearanceSnapshot snapshot)
|
||||||
|
{
|
||||||
|
OnAppearanceChangedCore(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override this to handle widget attachment.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void OnAttachedCore()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override this to handle widget detachment.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void OnDetachedCore()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override this to handle appearance changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="snapshot">New appearance snapshot</param>
|
||||||
|
protected virtual void OnAppearanceChangedCore(AirAppAppearanceSnapshot snapshot)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
96
LanMountainDesktop.AirAppSdk/AirAppWindowBase.cs
Normal file
96
LanMountainDesktop.AirAppSdk/AirAppWindowBase.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for AirApp windows.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class AirAppWindowBase : Window, IAirAppWindow
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the window descriptor.
|
||||||
|
/// Override this to customize window configuration.
|
||||||
|
/// </summary>
|
||||||
|
public virtual AirAppWindowDescriptor Descriptor => new()
|
||||||
|
{
|
||||||
|
Width = 800,
|
||||||
|
Height = 600,
|
||||||
|
MinWidth = 400,
|
||||||
|
MinHeight = 300,
|
||||||
|
ChromeMode = AirAppWindowChromeMode.Standard,
|
||||||
|
CanResize = true,
|
||||||
|
ShowInTaskbar = true,
|
||||||
|
ShowAsDialog = false
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of AirAppWindowBase.
|
||||||
|
/// </summary>
|
||||||
|
protected AirAppWindowBase()
|
||||||
|
{
|
||||||
|
ApplyDescriptor(Descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called before the window is opened.
|
||||||
|
/// </summary>
|
||||||
|
public virtual Task OnWindowOpeningAsync()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called after the window has been opened.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnWindowOpened()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the window is closing.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnWindowClosing(WindowClosingEventArgs e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called after the window has been closed.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnWindowClosed()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply the window descriptor configuration.
|
||||||
|
/// </summary>
|
||||||
|
private void ApplyDescriptor(AirAppWindowDescriptor descriptor)
|
||||||
|
{
|
||||||
|
Width = descriptor.Width;
|
||||||
|
Height = descriptor.Height;
|
||||||
|
MinWidth = descriptor.MinWidth;
|
||||||
|
MinHeight = descriptor.MinHeight;
|
||||||
|
CanResize = descriptor.CanResize;
|
||||||
|
ShowInTaskbar = descriptor.ShowInTaskbar;
|
||||||
|
ShowAsDialog = descriptor.ShowAsDialog;
|
||||||
|
|
||||||
|
// Apply chrome mode
|
||||||
|
switch (descriptor.ChromeMode)
|
||||||
|
{
|
||||||
|
case AirAppWindowChromeMode.Standard:
|
||||||
|
SystemDecorations = SystemDecorations.Full;
|
||||||
|
break;
|
||||||
|
case AirAppWindowChromeMode.Borderless:
|
||||||
|
SystemDecorations = SystemDecorations.BorderOnly;
|
||||||
|
break;
|
||||||
|
case AirAppWindowChromeMode.FullScreen:
|
||||||
|
SystemDecorations = SystemDecorations.None;
|
||||||
|
WindowState = WindowState.FullScreen;
|
||||||
|
break;
|
||||||
|
case AirAppWindowChromeMode.Tool:
|
||||||
|
SystemDecorations = SystemDecorations.Full;
|
||||||
|
ShowInTaskbar = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
LanMountainDesktop.AirAppSdk/AirAppWindowChromeMode.cs
Normal file
32
LanMountainDesktop.AirAppSdk/AirAppWindowChromeMode.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Window chrome mode for AirApp windows.
|
||||||
|
/// </summary>
|
||||||
|
public enum AirAppWindowChromeMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Standard window with title bar and borders.
|
||||||
|
/// </summary>
|
||||||
|
Standard = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Borderless window with custom chrome.
|
||||||
|
/// </summary>
|
||||||
|
Borderless = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Full-screen window with no decorations.
|
||||||
|
/// </summary>
|
||||||
|
FullScreen = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tool window (no taskbar icon, small title bar).
|
||||||
|
/// </summary>
|
||||||
|
Tool = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Background-only (no UI, reserved for future use).
|
||||||
|
/// </summary>
|
||||||
|
BackgroundOnly = 4
|
||||||
|
}
|
||||||
52
LanMountainDesktop.AirAppSdk/AirAppWindowDescriptor.cs
Normal file
52
LanMountainDesktop.AirAppSdk/AirAppWindowDescriptor.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Window configuration descriptor.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AirAppWindowDescriptor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the window title.
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; } = "AirApp Window";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the initial width.
|
||||||
|
/// </summary>
|
||||||
|
public double Width { get; set; } = 800;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the initial height.
|
||||||
|
/// </summary>
|
||||||
|
public double Height { get; set; } = 600;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the minimum width.
|
||||||
|
/// </summary>
|
||||||
|
public double MinWidth { get; set; } = 400;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the minimum height.
|
||||||
|
/// </summary>
|
||||||
|
public double MinHeight { get; set; } = 300;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the chrome mode.
|
||||||
|
/// </summary>
|
||||||
|
public AirAppWindowChromeMode ChromeMode { get; set; } = AirAppWindowChromeMode.Standard;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the window can be resized.
|
||||||
|
/// </summary>
|
||||||
|
public bool CanResize { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the window shows in the taskbar.
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowInTaskbar { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the window is modal.
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowAsDialog { get; set; } = false;
|
||||||
|
}
|
||||||
31
LanMountainDesktop.AirAppSdk/IAirApp.cs
Normal file
31
LanMountainDesktop.AirAppSdk/IAirApp.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Core interface for AirApp entry point.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAirApp
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize the AirApp and register services.
|
||||||
|
/// Called during host startup before the application is fully running.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Host builder context</param>
|
||||||
|
/// <param name="services">Service collection for dependency injection</param>
|
||||||
|
void Initialize(HostBuilderContext context, IServiceCollection services);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called after the host application has started.
|
||||||
|
/// Use this for initialization that requires runtime services.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">AirApp runtime context</param>
|
||||||
|
Task OnStartedAsync(IAirAppRuntimeContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the host application is stopping.
|
||||||
|
/// Use this for cleanup and resource disposal.
|
||||||
|
/// </summary>
|
||||||
|
Task OnStoppingAsync();
|
||||||
|
}
|
||||||
19
LanMountainDesktop.AirAppSdk/IAirAppAppearanceContext.cs
Normal file
19
LanMountainDesktop.AirAppSdk/IAirAppAppearanceContext.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides appearance and theme context.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAirAppAppearanceContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current appearance snapshot.
|
||||||
|
/// </summary>
|
||||||
|
AirAppAppearanceSnapshot CurrentSnapshot { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribe to appearance changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handler">Change handler</param>
|
||||||
|
/// <returns>Subscription token</returns>
|
||||||
|
IDisposable SubscribeToChanges(Action<AirAppAppearanceSnapshot> handler);
|
||||||
|
}
|
||||||
58
LanMountainDesktop.AirAppSdk/IAirAppComponentContext.cs
Normal file
58
LanMountainDesktop.AirAppSdk/IAirAppComponentContext.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Context provided to an AirApp desktop component instance.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAirAppComponentContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the component identifier.
|
||||||
|
/// </summary>
|
||||||
|
string ComponentId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the unique placement identifier for this component instance.
|
||||||
|
/// </summary>
|
||||||
|
string PlacementId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current width in grid cells.
|
||||||
|
/// </summary>
|
||||||
|
int Width { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current height in grid cells.
|
||||||
|
/// </summary>
|
||||||
|
int Height { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the service provider for this component.
|
||||||
|
/// </summary>
|
||||||
|
IServiceProvider Services { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the appearance context.
|
||||||
|
/// </summary>
|
||||||
|
IAirAppAppearanceContext Appearance { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request a window to be opened.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="windowId">Window identifier</param>
|
||||||
|
Task OpenWindowAsync(string windowId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a message to other components or AirApps.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic">Message topic</param>
|
||||||
|
/// <param name="payload">Message payload</param>
|
||||||
|
void SendMessage(string topic, object? payload = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribe to messages.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic">Message topic</param>
|
||||||
|
/// <param name="handler">Message handler</param>
|
||||||
|
/// <returns>Subscription token for unsubscribing</returns>
|
||||||
|
IDisposable Subscribe(string topic, Action<object?> handler);
|
||||||
|
}
|
||||||
37
LanMountainDesktop.AirAppSdk/IAirAppLogger.cs
Normal file
37
LanMountainDesktop.AirAppSdk/IAirAppLogger.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logger interface for AirApps.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAirAppLogger
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Log a debug message.
|
||||||
|
/// </summary>
|
||||||
|
void Debug(string message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log an informational message.
|
||||||
|
/// </summary>
|
||||||
|
void Info(string message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log a warning message.
|
||||||
|
/// </summary>
|
||||||
|
void Warn(string message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log a warning with exception.
|
||||||
|
/// </summary>
|
||||||
|
void Warn(string message, Exception exception);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log an error message.
|
||||||
|
/// </summary>
|
||||||
|
void Error(string message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log an error with exception.
|
||||||
|
/// </summary>
|
||||||
|
void Error(string message, Exception exception);
|
||||||
|
}
|
||||||
31
LanMountainDesktop.AirAppSdk/IAirAppMessageBus.cs
Normal file
31
LanMountainDesktop.AirAppSdk/IAirAppMessageBus.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message bus for inter-AirApp communication.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAirAppMessageBus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Publish a message to a topic.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic">Message topic</param>
|
||||||
|
/// <param name="payload">Message payload</param>
|
||||||
|
void Publish(string topic, object? payload = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribe to a topic.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic">Message topic</param>
|
||||||
|
/// <param name="handler">Message handler</param>
|
||||||
|
/// <returns>Subscription token</returns>
|
||||||
|
IDisposable Subscribe(string topic, Action<object?> handler);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribe to a topic with typed payload.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Payload type</typeparam>
|
||||||
|
/// <param name="topic">Message topic</param>
|
||||||
|
/// <param name="handler">Typed message handler</param>
|
||||||
|
/// <returns>Subscription token</returns>
|
||||||
|
IDisposable Subscribe<T>(string topic, Action<T?> handler);
|
||||||
|
}
|
||||||
91
LanMountainDesktop.AirAppSdk/IAirAppRuntimeContext.cs
Normal file
91
LanMountainDesktop.AirAppSdk/IAirAppRuntimeContext.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides runtime context and services for an AirApp.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAirAppRuntimeContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the unique identifier of this AirApp.
|
||||||
|
/// </summary>
|
||||||
|
string AirAppId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the display name of this AirApp.
|
||||||
|
/// </summary>
|
||||||
|
string AirAppName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the AirApp version.
|
||||||
|
/// </summary>
|
||||||
|
string AirAppVersion { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the data directory for this AirApp.
|
||||||
|
/// Use this directory to store persistent user data.
|
||||||
|
/// </summary>
|
||||||
|
string DataDirectory { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the cache directory for this AirApp.
|
||||||
|
/// Use this directory to store temporary cached data.
|
||||||
|
/// </summary>
|
||||||
|
string CacheDirectory { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the service provider for dependency injection.
|
||||||
|
/// </summary>
|
||||||
|
IServiceProvider Services { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the host application lifetime manager.
|
||||||
|
/// </summary>
|
||||||
|
IHostApplicationLifetime Lifetime { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the message bus for inter-AirApp communication.
|
||||||
|
/// </summary>
|
||||||
|
IAirAppMessageBus MessageBus { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the appearance context for theme and styling.
|
||||||
|
/// </summary>
|
||||||
|
IAirAppAppearanceContext Appearance { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the logger for this AirApp.
|
||||||
|
/// </summary>
|
||||||
|
IAirAppLogger Logger { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens a window defined by this AirApp.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="windowId">Window identifier</param>
|
||||||
|
/// <returns>The opened window instance</returns>
|
||||||
|
Task<IAirAppWindow> OpenWindowAsync(string windowId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes a window by its identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="windowId">Window identifier</param>
|
||||||
|
void CloseWindow(string windowId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a desktop component (internal use by AirAppBase).
|
||||||
|
/// </summary>
|
||||||
|
void RegisterComponent(AirAppComponentOptions options);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a window (internal use by AirAppBase).
|
||||||
|
/// </summary>
|
||||||
|
void RegisterWindow(string id, string name, Type windowType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a service (internal use by AirAppBase).
|
||||||
|
/// </summary>
|
||||||
|
void RegisterService<TService, TImplementation>()
|
||||||
|
where TService : class
|
||||||
|
where TImplementation : class, TService;
|
||||||
|
}
|
||||||
29
LanMountainDesktop.AirAppSdk/IAirAppWidget.cs
Normal file
29
LanMountainDesktop.AirAppSdk/IAirAppWidget.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for AirApp desktop component widgets.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAirAppWidget
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the component context.
|
||||||
|
/// Set by the host when the widget is created.
|
||||||
|
/// </summary>
|
||||||
|
IAirAppComponentContext Context { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the widget is attached to the desktop.
|
||||||
|
/// </summary>
|
||||||
|
void OnAttached();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the widget is detached from the desktop.
|
||||||
|
/// </summary>
|
||||||
|
void OnDetached();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the appearance (theme) has changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="snapshot">New appearance snapshot</param>
|
||||||
|
void OnAppearanceChanged(AirAppAppearanceSnapshot snapshot);
|
||||||
|
}
|
||||||
36
LanMountainDesktop.AirAppSdk/IAirAppWindow.cs
Normal file
36
LanMountainDesktop.AirAppSdk/IAirAppWindow.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for AirApp windows.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAirAppWindow
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the window descriptor (configuration).
|
||||||
|
/// </summary>
|
||||||
|
AirAppWindowDescriptor Descriptor { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called before the window is opened.
|
||||||
|
/// Use this for async initialization.
|
||||||
|
/// </summary>
|
||||||
|
Task OnWindowOpeningAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called after the window has been opened.
|
||||||
|
/// </summary>
|
||||||
|
void OnWindowOpened();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the window is closing.
|
||||||
|
/// Set e.Cancel = true to prevent closing.
|
||||||
|
/// </summary>
|
||||||
|
void OnWindowClosing(WindowClosingEventArgs e);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called after the window has been closed.
|
||||||
|
/// </summary>
|
||||||
|
void OnWindowClosed();
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
|
||||||
|
<!-- Package metadata -->
|
||||||
|
<PackageId>LanMountainDesktop.AirAppSdk</PackageId>
|
||||||
|
<Version>6.0.0</Version>
|
||||||
|
<Authors>LanMountainDesktop Team</Authors>
|
||||||
|
<Description>Official SDK for developing AirApps (Lightweight Applications) for LanMountainDesktop</Description>
|
||||||
|
<PackageTags>lanmountaindesktop;airapp;sdk;plugin;avalonia</PackageTags>
|
||||||
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
<RepositoryUrl>https://github.com/LanMountain/LanMountainDesktop</RepositoryUrl>
|
||||||
|
|
||||||
|
<!-- Build transitive: include packaging targets in consuming projects -->
|
||||||
|
<BuildTransitive>true</BuildTransitive>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Avalonia" Version="11.2.2" />
|
||||||
|
<PackageReference Include="Avalonia.Controls" Version="11.2.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Build targets for .laapp packaging -->
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="build\**" Pack="true" PackagePath="build\" />
|
||||||
|
<None Include="buildTransitive\**" Pack="true" PackagePath="buildTransitive\" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
363
LanMountainDesktop.AirAppSdk/README.md
Normal file
363
LanMountainDesktop.AirAppSdk/README.md
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
# LanMountainDesktop.AirAppSdk
|
||||||
|
|
||||||
|
Official SDK for developing AirApps (Lightweight Applications) for LanMountainDesktop.
|
||||||
|
|
||||||
|
## What is an AirApp?
|
||||||
|
|
||||||
|
AirApp is the next-generation application framework for LanMountainDesktop. It provides a unified development experience for creating:
|
||||||
|
|
||||||
|
- **Desktop Components** - Widgets that live on the desktop
|
||||||
|
- **Window Applications** - Standalone windowed apps
|
||||||
|
- **Background Services** - Services that run in the background
|
||||||
|
- **Hybrid Apps** - Apps that combine multiple modes
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install the SDK package
|
||||||
|
dotnet add package LanMountainDesktop.AirAppSdk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Your First AirApp
|
||||||
|
|
||||||
|
1. **Create a new project**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet new classlib -n MyFirstAirApp
|
||||||
|
cd MyFirstAirApp
|
||||||
|
dotnet add package LanMountainDesktop.AirAppSdk
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create the entry point**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using LanMountainDesktop.AirAppSdk;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace MyFirstAirApp;
|
||||||
|
|
||||||
|
[AirAppEntrance]
|
||||||
|
public class MyAirApp : AirAppBase
|
||||||
|
{
|
||||||
|
public override void Initialize(HostBuilderContext context, IServiceCollection services)
|
||||||
|
{
|
||||||
|
// Register a desktop component
|
||||||
|
services.AddAirAppComponent<MyWidget>("my-widget", "My Widget");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create a widget**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
namespace MyFirstAirApp;
|
||||||
|
|
||||||
|
public class MyWidget : AirAppWidgetBase
|
||||||
|
{
|
||||||
|
public MyWidget()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
// Simple widget with a TextBlock
|
||||||
|
Content = new TextBlock
|
||||||
|
{
|
||||||
|
Text = "Hello from AirApp!",
|
||||||
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnAttachedCore()
|
||||||
|
{
|
||||||
|
// Called when widget is added to desktop
|
||||||
|
Context.Logger.Info("My widget attached!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Create manifest file** (`airapp.json`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "com.example.myfirstairapp",
|
||||||
|
"name": "My First AirApp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"apiVersion": "6.0.0",
|
||||||
|
"author": "Your Name",
|
||||||
|
"description": "My first AirApp for LanMountainDesktop",
|
||||||
|
"entranceAssembly": "MyFirstAirApp.dll",
|
||||||
|
"runtime": {
|
||||||
|
"mode": "in-process"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Build the project**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
This will produce a `.laapp` package in `bin/Release/net10.0/MyFirstAirApp.laapp`.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### AirAppBase
|
||||||
|
|
||||||
|
The entry point for your AirApp. Override `Initialize()` to register components and services:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[AirAppEntrance]
|
||||||
|
public class MyAirApp : AirAppBase
|
||||||
|
{
|
||||||
|
public override void Initialize(HostBuilderContext context, IServiceCollection services)
|
||||||
|
{
|
||||||
|
// Register components
|
||||||
|
services.AddAirAppComponent<MyWidget>("widget-id", "Widget Name");
|
||||||
|
|
||||||
|
// Register windows
|
||||||
|
services.AddAirAppWindow<MyWindow>("window-id", "Window Name");
|
||||||
|
|
||||||
|
// Register your services
|
||||||
|
services.AddSingleton<IMyService, MyService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnStartedAsync(IAirAppRuntimeContext context)
|
||||||
|
{
|
||||||
|
// Runtime initialization
|
||||||
|
context.Logger.Info("AirApp started!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Desktop Components
|
||||||
|
|
||||||
|
Create widgets that appear on the desktop:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ClockWidget : AirAppWidgetBase
|
||||||
|
{
|
||||||
|
private TextBlock _timeText;
|
||||||
|
|
||||||
|
public ClockWidget()
|
||||||
|
{
|
||||||
|
_timeText = new TextBlock();
|
||||||
|
Content = _timeText;
|
||||||
|
|
||||||
|
// Update every second
|
||||||
|
DispatcherTimer.Run(() =>
|
||||||
|
{
|
||||||
|
_timeText.Text = DateTime.Now.ToString("HH:mm:ss");
|
||||||
|
return true;
|
||||||
|
}, TimeSpan.FromSeconds(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnAppearanceChangedCore(AirAppAppearanceSnapshot snapshot)
|
||||||
|
{
|
||||||
|
// Respond to theme changes
|
||||||
|
_timeText.Foreground = new SolidColorBrush(snapshot.ForegroundColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
Create standalone windows:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class MyWindow : AirAppWindowBase
|
||||||
|
{
|
||||||
|
public override AirAppWindowDescriptor Descriptor => new()
|
||||||
|
{
|
||||||
|
Title = "My Window",
|
||||||
|
Width = 800,
|
||||||
|
Height = 600,
|
||||||
|
ChromeMode = AirAppWindowChromeMode.Standard,
|
||||||
|
CanResize = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public MyWindow()
|
||||||
|
{
|
||||||
|
Content = new TextBlock { Text = "Hello from window!" };
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnWindowOpeningAsync()
|
||||||
|
{
|
||||||
|
// Async initialization before window opens
|
||||||
|
await LoadDataAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Runtime Context
|
||||||
|
|
||||||
|
Access runtime services:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
protected override async Task OnStartedAsync(IAirAppRuntimeContext context)
|
||||||
|
{
|
||||||
|
// Get data directories
|
||||||
|
var dataDir = context.DataDirectory;
|
||||||
|
var cacheDir = context.CacheDirectory;
|
||||||
|
|
||||||
|
// Use services
|
||||||
|
var myService = context.Services.GetService<IMyService>();
|
||||||
|
|
||||||
|
// Log messages
|
||||||
|
context.Logger.Info("AirApp started!");
|
||||||
|
|
||||||
|
// Open a window
|
||||||
|
await context.OpenWindowAsync("my-window");
|
||||||
|
|
||||||
|
// Subscribe to messages
|
||||||
|
context.MessageBus.Subscribe("theme-changed", payload =>
|
||||||
|
{
|
||||||
|
context.Logger.Info("Theme changed!");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Core Interfaces
|
||||||
|
|
||||||
|
- `IAirApp` - AirApp entry point
|
||||||
|
- `IAirAppWidget` - Desktop component widget
|
||||||
|
- `IAirAppWindow` - Window application
|
||||||
|
- `IAirAppRuntimeContext` - Runtime services and context
|
||||||
|
- `IAirAppComponentContext` - Component instance context
|
||||||
|
|
||||||
|
### Base Classes
|
||||||
|
|
||||||
|
- `AirAppBase` - Base implementation of IAirApp
|
||||||
|
- `AirAppWidgetBase` - Base class for widgets
|
||||||
|
- `AirAppWindowBase` - Base class for windows
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
- `AirAppManifest` - Manifest file structure
|
||||||
|
- `AirAppComponentOptions` - Component registration options
|
||||||
|
- `AirAppWindowDescriptor` - Window configuration
|
||||||
|
- `AirAppRuntimeMode` - Runtime isolation modes
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
- `IAirAppLogger` - Logging service
|
||||||
|
- `IAirAppMessageBus` - Inter-app messaging
|
||||||
|
- `IAirAppAppearanceContext` - Theme and appearance
|
||||||
|
|
||||||
|
## Runtime Modes
|
||||||
|
|
||||||
|
### In-Process (Default)
|
||||||
|
|
||||||
|
Best performance, runs in the host process:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"runtime": {
|
||||||
|
"mode": "in-process"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Isolated Background
|
||||||
|
|
||||||
|
Runs in a separate background process:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"runtime": {
|
||||||
|
"mode": "isolated-background"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Isolated Window
|
||||||
|
|
||||||
|
Runs in a completely isolated window process:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"runtime": {
|
||||||
|
"mode": "isolated-window"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Packaging
|
||||||
|
|
||||||
|
Your AirApp is automatically packaged as a `.laapp` file when you build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
The package includes:
|
||||||
|
- All assemblies
|
||||||
|
- The `airapp.json` manifest
|
||||||
|
- Any additional resources
|
||||||
|
|
||||||
|
## Migration from Plugin SDK v5
|
||||||
|
|
||||||
|
If you're migrating from the older Plugin SDK:
|
||||||
|
|
||||||
|
1. Update package reference:
|
||||||
|
```xml
|
||||||
|
<!-- Old -->
|
||||||
|
<PackageReference Include="LanMountainDesktop.PluginSdk" Version="5.0.0" />
|
||||||
|
|
||||||
|
<!-- New -->
|
||||||
|
<PackageReference Include="LanMountainDesktop.AirAppSdk" Version="6.0.0" />
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update manifest file: `plugin.json` → `airapp.json`
|
||||||
|
|
||||||
|
3. Update namespaces:
|
||||||
|
```csharp
|
||||||
|
// Old
|
||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
|
[PluginEntrance]
|
||||||
|
public class Plugin : PluginBase { }
|
||||||
|
|
||||||
|
// New
|
||||||
|
using LanMountainDesktop.AirAppSdk;
|
||||||
|
[AirAppEntrance]
|
||||||
|
public class MyAirApp : AirAppBase { }
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Update API calls (mostly compatible, minor naming changes)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See the `samples/` directory for complete examples:
|
||||||
|
|
||||||
|
- **SimpleWidget** - Basic desktop component
|
||||||
|
- **ClockWidget** - Time display with auto-update
|
||||||
|
- **WindowApp** - Standalone window application
|
||||||
|
- **HybridApp** - Component + window combination
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Full API Documentation](https://docs.lanmountain.com/airapp-sdk)
|
||||||
|
- [Development Guide](https://docs.lanmountain.com/airapp-dev-guide)
|
||||||
|
- [Best Practices](https://docs.lanmountain.com/airapp-best-practices)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- GitHub Issues: https://github.com/LanMountain/LanMountainDesktop/issues
|
||||||
|
- Discord: https://discord.gg/lanmountain
|
||||||
|
- Documentation: https://docs.lanmountain.com
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - See LICENSE file for details
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<PackageType>Template</PackageType>
|
||||||
|
<PackageVersion>6.0.0</PackageVersion>
|
||||||
|
<PackageId>LanMountainDesktop.AirAppTemplate</PackageId>
|
||||||
|
<Title>LanMountainDesktop AirApp Templates</Title>
|
||||||
|
<Authors>LanMountainDesktop Team</Authors>
|
||||||
|
<Description>Project templates for creating AirApps for LanMountainDesktop</Description>
|
||||||
|
<PackageTags>templates;lanmountaindesktop;airapp;dotnet-new</PackageTags>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<IncludeContentInPack>true</IncludeContentInPack>
|
||||||
|
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||||
|
<ContentTargetFolders>content</ContentTargetFolders>
|
||||||
|
<NoWarn>$(NoWarn);NU5128</NoWarn>
|
||||||
|
<NoDefaultExcludes>true</NoDefaultExcludes>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="templates\**\*" Exclude="templates\**\bin\**;templates\**\obj\**" />
|
||||||
|
<Compile Remove="**\*" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/template",
|
||||||
|
"author": "LanMountainDesktop Team",
|
||||||
|
"classifications": ["LanMountainDesktop", "AirApp", "Component"],
|
||||||
|
"identity": "LanMountainDesktop.AirApp.Component",
|
||||||
|
"name": "LanMountainDesktop AirApp - Desktop Component",
|
||||||
|
"shortName": "lmd-airapp-component",
|
||||||
|
"tags": {
|
||||||
|
"language": "C#",
|
||||||
|
"type": "project"
|
||||||
|
},
|
||||||
|
"sourceName": "LanMountainDesktop.AirApp.ComponentTemplate",
|
||||||
|
"preferNameDirectory": true,
|
||||||
|
"symbols": {
|
||||||
|
"ComponentId": {
|
||||||
|
"type": "parameter",
|
||||||
|
"datatype": "string",
|
||||||
|
"defaultValue": "my-widget",
|
||||||
|
"replaces": "my-widget",
|
||||||
|
"description": "The unique identifier for the component"
|
||||||
|
},
|
||||||
|
"ComponentName": {
|
||||||
|
"type": "parameter",
|
||||||
|
"datatype": "string",
|
||||||
|
"defaultValue": "My Widget",
|
||||||
|
"replaces": "My Widget",
|
||||||
|
"description": "The display name for the component"
|
||||||
|
},
|
||||||
|
"AuthorName": {
|
||||||
|
"type": "parameter",
|
||||||
|
"datatype": "string",
|
||||||
|
"defaultValue": "Your Name",
|
||||||
|
"replaces": "Your Name",
|
||||||
|
"description": "The author name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="LanMountainDesktop.AirAppSdk" Version="6.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Include airapp.json in output -->
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="airapp.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using LanMountainDesktop.AirAppSdk;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirApp.ComponentTemplate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AirApp entry point.
|
||||||
|
/// </summary>
|
||||||
|
[AirAppEntrance]
|
||||||
|
public sealed class MyAirApp : AirAppBase
|
||||||
|
{
|
||||||
|
public override void Initialize(HostBuilderContext context, IServiceCollection services)
|
||||||
|
{
|
||||||
|
// Register the desktop component
|
||||||
|
services.AddAirAppComponent<MyWidget>(
|
||||||
|
"my-widget",
|
||||||
|
"My Widget",
|
||||||
|
options =>
|
||||||
|
{
|
||||||
|
options.Description = "A sample desktop component";
|
||||||
|
options.DefaultWidth = 2;
|
||||||
|
options.DefaultHeight = 2;
|
||||||
|
options.ResizeMode = AirAppComponentResizeMode.Both;
|
||||||
|
options.Category = "Custom";
|
||||||
|
options.IconKey = "AppGeneric";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task OnStartedAsync(IAirAppRuntimeContext context)
|
||||||
|
{
|
||||||
|
context.Logger.Info("My AirApp started successfully!");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task OnStoppingAsync()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using LanMountainDesktop.AirAppSdk;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.AirApp.ComponentTemplate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desktop component widget implementation.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MyWidget : AirAppWidgetBase
|
||||||
|
{
|
||||||
|
private readonly TextBlock _titleText;
|
||||||
|
private readonly TextBlock _timeText;
|
||||||
|
private readonly DispatcherTimer _timer;
|
||||||
|
|
||||||
|
public MyWidget()
|
||||||
|
{
|
||||||
|
// Create UI
|
||||||
|
_titleText = new TextBlock
|
||||||
|
{
|
||||||
|
Text = "My Widget",
|
||||||
|
FontSize = 16,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
_timeText = new TextBlock
|
||||||
|
{
|
||||||
|
FontSize = 24,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
var panel = new StackPanel
|
||||||
|
{
|
||||||
|
Spacing = 8,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
panel.Children.Add(_titleText);
|
||||||
|
panel.Children.Add(_timeText);
|
||||||
|
|
||||||
|
Content = panel;
|
||||||
|
|
||||||
|
// Setup timer to update time
|
||||||
|
_timer = new DispatcherTimer
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(1)
|
||||||
|
};
|
||||||
|
_timer.Tick += (s, e) => UpdateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnAttachedCore()
|
||||||
|
{
|
||||||
|
Context.Logger.Info($"Widget attached: {Context.ComponentId} at {Context.PlacementId}");
|
||||||
|
UpdateTime();
|
||||||
|
_timer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDetachedCore()
|
||||||
|
{
|
||||||
|
Context.Logger.Info($"Widget detached: {Context.ComponentId}");
|
||||||
|
_timer.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnAppearanceChangedCore(AirAppAppearanceSnapshot snapshot)
|
||||||
|
{
|
||||||
|
// Respond to theme changes
|
||||||
|
_titleText.Foreground = new SolidColorBrush(snapshot.ForegroundColor);
|
||||||
|
_timeText.Foreground = new SolidColorBrush(snapshot.AccentColor);
|
||||||
|
|
||||||
|
Context.Logger.Info($"Appearance changed: DarkMode={snapshot.IsDarkMode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTime()
|
||||||
|
{
|
||||||
|
_timeText.Text = DateTime.Now.ToString("HH:mm:ss");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# LanMountainDesktop.AirApp.ComponentTemplate
|
||||||
|
|
||||||
|
A desktop component AirApp for LanMountainDesktop.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
This will produce a `.laapp` package in `bin/Release/net10.0/`.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Copy the `.laapp` file to LanMountainDesktop's plugins directory or install via the AirApp Market.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
To test your component during development:
|
||||||
|
|
||||||
|
1. Build the project
|
||||||
|
2. Run LanMountainDesktop with debug mode:
|
||||||
|
```bash
|
||||||
|
dotnet run --project path/to/LanMountainDesktop.csproj -- --debug-airapp path/to/your/bin/Debug/net10.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customize
|
||||||
|
|
||||||
|
- Edit `MyWidget.cs` to modify the component UI and behavior
|
||||||
|
- Edit `airapp.json` to change metadata
|
||||||
|
- Add more components by creating additional widget classes and registering them in `MyAirApp.cs`
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"id": "com.example.LanMountainDesktop.AirApp.ComponentTemplate",
|
||||||
|
"name": "LanMountainDesktop.AirApp.ComponentTemplate",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"apiVersion": "6.0.0",
|
||||||
|
"author": "Your Name",
|
||||||
|
"description": "A desktop component AirApp for LanMountainDesktop",
|
||||||
|
"entranceAssembly": "LanMountainDesktop.AirApp.ComponentTemplate.dll",
|
||||||
|
"runtime": {
|
||||||
|
"mode": "in-process",
|
||||||
|
"capabilities": ["desktop-component"]
|
||||||
|
},
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"id": "my-widget",
|
||||||
|
"name": "My Widget",
|
||||||
|
"defaultWidth": 2,
|
||||||
|
"defaultHeight": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user