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