Files
2026-11-04 14:29:22 -06:00

360 lines
13 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Windows;
namespace LiveWall.Sdk;
/// <summary>Version of the plugin API. Bumped when contracts change in an incompatible way.</summary>
public static class PluginApi
{
/// <summary>Current contract version. Plugins must declare a compatible version in plugin.json.</summary>
public const string Version = "1.0";
/// <summary>Host application version this SDK shipped with (informational).</summary>
public const string HostVersion = "1.8.4";
}
/// <summary>
/// What a plugin is allowed to do, declared in its <c>plugin.json</c> and shown to the user before
/// the plugin is enabled. These are declarations, not a sandbox: a plugin runs inside LiveWall's
/// process with full .NET rights. They exist so the user knows what they are agreeing to.
/// </summary>
[Flags]
public enum PluginPermission
{
None = 0,
/// <summary>Read the current wallpaper, playback state and a snapshot of the settings.</summary>
ReadWallpaper = 1 << 0,
/// <summary>Play / pause / next / set the wallpaper.</summary>
ControlPlayback = 1 << 1,
/// <summary>Draw a layer on top of the wallpaper (clocks, visualisers, badges…).</summary>
DesktopOverlay = 1 << 2,
/// <summary>Contribute pages to the LiveWall settings window.</summary>
SettingsUi = 1 << 3,
/// <summary>Contribute commands the user can run from the app.</summary>
Commands = 1 << 4,
/// <summary>Contribute entries to the tray menu.</summary>
TrayMenu = 1 << 5,
/// <summary>Register global hotkeys.</summary>
Hotkeys = 1 << 6,
/// <summary>Read and write files (the plugin's data folder, and anything the user points it at).</summary>
FileSystem = 1 << 7,
/// <summary>Make outbound network requests.</summary>
Network = 1 << 8,
/// <summary>Supply a wallpaper renderer for sources it claims (overrides the built-in players).</summary>
Renderer = 1 << 9,
/// <summary>Rewrite or veto wallpaper requests before the engine applies them.</summary>
Filters = 1 << 10,
All = ReadWallpaper | ControlPlayback | DesktopOverlay | SettingsUi | Commands | TrayMenu |
Hotkeys | FileSystem | Network | Renderer | Filters
}
/// <summary>Human readable description of what every permission means (used by the warning dialog).</summary>
public static class PluginPermissionInfo
{
public static string Describe(PluginPermission permission) => permission switch
{
PluginPermission.ReadWallpaper => "Read your current wallpaper, playback state and settings",
PluginPermission.ControlPlayback => "Start, stop, skip and replace your wallpaper",
PluginPermission.DesktopOverlay => "Draw content on top of your desktop wallpaper",
PluginPermission.SettingsUi => "Add pages to the LiveWall settings window",
PluginPermission.Commands => "Add commands to LiveWall",
PluginPermission.TrayMenu => "Add entries to the LiveWall tray menu",
PluginPermission.Hotkeys => "Register system-wide keyboard shortcuts",
PluginPermission.FileSystem => "Read and write files on your computer",
PluginPermission.Network => "Connect to the internet",
PluginPermission.Renderer => "Take over wallpaper playback with its own renderer",
PluginPermission.Filters => "Rewrite or block wallpaper changes",
_ => permission.ToString()
};
/// <summary>Expands a flag set into individual descriptions.</summary>
public static IReadOnlyList<(PluginPermission Permission, string Description)> Expand(PluginPermission permissions)
{
var list = new List<(PluginPermission, string)>();
foreach (PluginPermission p in Enum.GetValues<PluginPermission>())
{
if (p is PluginPermission.None or PluginPermission.All) continue;
if (permissions.HasFlag(p)) list.Add((p, Describe(p)));
}
return list;
}
}
/// <summary>Everything the host knows about a plugin. Filled in by LiveWall before Initialize.</summary>
public sealed class PluginInfo
{
public string Id { get; init; } = "";
public string Name { get; init; } = "";
public string Version { get; init; } = "";
public string Author { get; init; } = "";
public string Description { get; init; } = "";
public string Homepage { get; init; } = "";
public string ApiVersion { get; init; } = "";
public PluginPermission Permissions { get; init; }
/// <summary>Absolute path of the plugin folder.</summary>
public string Folder { get; init; } = "";
/// <summary>Absolute path of the plugin assembly.</summary>
public string AssemblyPath { get; init; } = "";
/// <summary>SHA-256 of the plugin assembly (lower case hex).</summary>
public string Sha256 { get; init; } = "";
/// <summary>
/// True when the loaded assembly hash matches the hash the user trusted on this machine, or the
/// publisher ships it in LiveWall's built-in trusted list. Unverified plugins run with a warning.
/// </summary>
public bool Verified { get; init; }
}
/// <summary>The plugin entry point. One public class implementing this per assembly.</summary>
public interface ILiveWallPlugin
{
/// <summary>
/// Called once, on the UI thread, after the user enabled the plugin. Register your extensions
/// and subscribe to events here. Exceptions are caught, logged and disable the plugin.
/// </summary>
void Initialize(IPluginContext context);
/// <summary>Called when the plugin is disabled, reloaded or the app is closing.</summary>
void Shutdown();
}
/// <summary>Everything a plugin can reach. One instance per plugin.</summary>
public interface IPluginContext
{
PluginInfo Info { get; }
/// <summary>Writes to the LiveWall log next to the host's own messages.</summary>
IPluginLog Log { get; }
/// <summary>Persistent key/value storage owned by the plugin (JSON file in its data folder).</summary>
IPluginStorage Storage { get; }
/// <summary>Read-only snapshot of the user's configuration.</summary>
ISettingsSnapshot Settings { get; }
/// <summary>Actions a plugin may trigger in the host.</summary>
IHostActions Host { get; }
/// <summary>Type based publish/subscribe bus.</summary>
IPluginEvents Events { get; }
/// <summary>Registers extension points (overlays, pages, commands, renderers, filters, hotkeys).</summary>
IPluginRegistrar Extensions { get; }
/// <summary>Cancelled when the plugin is being shut down. Pass it to your async work.</summary>
CancellationToken ShutdownToken { get; }
}
/// <summary>Logging façade. Everything lands in %APPDATA%\LiveWall\logs.</summary>
public interface IPluginLog
{
void Debug(string message);
void Info(string message);
void Warn(string message);
void Error(string message, Exception? exception = null);
}
/// <summary>
/// Per plugin JSON storage. Values are serialised with System.Text.Json, so any POCO works.
/// </summary>
public interface IPluginStorage
{
/// <summary>The plugin's private data folder (created on demand).</summary>
string DataFolder { get; }
T? Get<T>(string key, T? fallback = default);
void Set<T>(string key, T value);
bool Remove(string key);
IReadOnlyCollection<string> Keys { get; }
/// <summary>Absolute path inside the data folder; the directory is created for you.</summary>
string PathFor(string fileName);
/// <summary>Writes pending changes immediately (normally done automatically).</summary>
void Flush();
}
/// <summary>Read-only view of the live settings.</summary>
public interface ISettingsSnapshot
{
Guid SnapshotId { get; }
string SourceKind { get; } // "Video", "AnimatedGif", "Image", "YouTube", "WebPage"
string SourceLocation { get; } // file path or URL
bool Muted { get; }
double Volume { get; } // 0–100
double Speed { get; } // 0.25–4
bool Loop { get; }
bool InteractiveMode { get; }
bool Paused { get; }
int DimPercent { get; }
string DimColor { get; }
IReadOnlyList<string> DisplayDevices { get; }
}
/// <summary>The current wallpaper, as seen by plugins.</summary>
public sealed class WallpaperState
{
public string Kind { get; init; } = "";
public string Source { get; init; } = "";
public string Title { get; init; } = "";
public bool IsPlaying { get; init; }
public bool IsPaused { get; init; }
public bool IsInteractive { get; init; }
public bool SuppliedByPlugin { get; init; }
public string RendererName { get; init; } = "";
}
/// <summary>Host actions. Each one is a no-op when the plugin lacks the permission for it.</summary>
public interface IHostActions
{
WallpaperState Current { get; }
/// <summary>Replaces the wallpaper (needs <see cref="PluginPermission.ControlPlayback"/>).</summary>
void SetWallpaper(WallpaperRequest request);
void Play();
void Pause();
void TogglePause();
void NextWallpaper();
void ReloadWallpaper();
void Reattach();
/// <summary>Opens the LiveWall settings window.</summary>
void OpenSettings();
/// <summary>Status line in the settings window.</summary>
void SetStatus(string text);
/// <summary>Balloon notification (respects the user's notification setting).</summary>
void ShowNotification(string title, string message);
/// <summary>Shows or hides the desktop overlay layer for this plugin.</summary>
void SetOverlayVisible(bool visible);
}
/// <summary>A wallpaper a plugin wants to show.</summary>
public sealed class WallpaperRequest
{
/// <summary>"Video", "AnimatedGif", "Image", "YouTube", "WebPage" or "Custom".</summary>
public string Kind { get; set; } = "Custom";
/// <summary>File path, URL or any string your renderer interprets.</summary>
public string Source { get; set; } = "";
public string Title { get; set; } = "";
/// <summary>Free-form data for your own renderer (survives filters, not persisted).</summary>
public Dictionary<string, string> Metadata { get; } = new(StringComparer.OrdinalIgnoreCase);
public WallpaperRequest Clone()
{
var clone = new WallpaperRequest { Kind = Kind, Source = Source, Title = Title };
foreach (var pair in Metadata) clone.Metadata[pair.Key] = pair.Value;
return clone;
}
public override string ToString() => $"{Kind}:{Source}";
}
/// <summary>Registers extension points. All calls are thread safe and may happen in Initialize.</summary>
public interface IPluginRegistrar
{
/// <summary>Draws above the wallpaper (needs <see cref="PluginPermission.DesktopOverlay"/>).</summary>
void AddOverlay(IWallpaperOverlay overlay);
void RemoveOverlay(IWallpaperOverlay overlay);
/// <summary>Adds a page to the Plugins tab (needs <see cref="PluginPermission.SettingsUi"/>).</summary>
void AddSettingsPage(IPluginSettingsPage page);
/// <summary>Adds a command to the app and the tray menu.</summary>
void AddCommand(PluginCommand command);
/// <summary>Adds a wallpaper filter (needs <see cref="PluginPermission.Filters"/>).</summary>
void AddFilter(IWallpaperFilter filter);
/// <summary>Takes over rendering of sources it claims (needs <see cref="PluginPermission.Renderer"/>).</summary>
void AddRenderer(IWallpaperRendererPlugin renderer);
/// <summary>Registers a global hotkey such as "Ctrl+Alt+K" (needs <see cref="PluginPermission.Hotkeys"/>).</summary>
void AddHotkey(PluginHotkey hotkey);
}
/// <summary>An entry in the app's plugin command list and the tray menu.</summary>
public sealed class PluginCommand
{
public PluginCommand(string id, string title, Action<PluginCommandContext> execute)
{
Id = id;
Title = title;
Execute = execute;
}
public string Id { get; }
public string Title { get; set; }
/// <summary>Optional grouping label, e.g. "Wallpaper".</summary>
public string Category { get; set; } = "";
/// <summary>Return false to disable the entry (evaluated when the menu opens).</summary>
public Func<bool>? CanExecute { get; set; }
internal Action<PluginCommandContext> Execute { get; }
public void Run(PluginCommandContext context) => Execute(context);
}
/// <summary>Passed to <see cref="PluginCommand"/>.</summary>
public sealed class PluginCommandContext
{
public PluginCommandContext(IPluginContext plugin, IHostActions host)
{
Plugin = plugin;
Host = host;
}
public IPluginContext Plugin { get; }
public IHostActions Host { get; }
}
/// <summary>A global keyboard shortcut contributed by a plugin.</summary>
public sealed class PluginHotkey
{
public PluginHotkey(string id, string gesture, Action action)
{
Id = id;
Gesture = gesture;
Action = action;
}
public string Id { get; }
/// <summary>Combination such as "Ctrl+Alt+K", parsed like LiveWall's own hotkeys.</summary>
public string Gesture { get; }
public string Description { get; set; } = "";
public Action Action { get; }
}
/// <summary>Raised by plugins to inform others about something (or to listen).</summary>
public interface IPluginEvents
{
IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class;
void Publish<TEvent>(TEvent payload) where TEvent : class;
}