feat.airapp sdk

This commit is contained in:
lincube
2026-06-08 02:39:44 +08:00
parent 1a6f129e78
commit 7db72fbcd0
36 changed files with 2617 additions and 0 deletions

View 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; }
}

View 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>();
}
}

View 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; }
}

View 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
}

View 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
}

View 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
{
}

View 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>()
};
}
}

View 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;
}
}

View 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";
}

View File

@@ -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; }
}

View 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)
{
}
}

View 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;
}
}
}

View 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
}

View 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;
}

View 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();
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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;
}

View 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);
}

View 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();
}

View File

@@ -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>

View 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