initial commit
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
# API reference (`LiveWall.Sdk` 1.0)
|
||||
|
||||
Everything below lives in the namespace `LiveWall.Sdk`, in the single assembly `LiveWall.Sdk.dll`
|
||||
(target `net8.0-windows`, WPF). Nullable reference types are enabled.
|
||||
|
||||
---
|
||||
|
||||
## `PluginApi` (static)
|
||||
|
||||
| Member | Value | Meaning |
|
||||
|---|---|---|
|
||||
| `Version` | `"1.0"` | Contract version. Must match the `apiVersion` in `plugin.json`. |
|
||||
| `HostVersion` | e.g. `"1.8.0"` | LiveWall version this SDK shipped with. Informational. |
|
||||
|
||||
## `PluginPermission` (`[Flags]`)
|
||||
|
||||
| Flag | Dialog text |
|
||||
|---|---|
|
||||
| `None` | — |
|
||||
| `ReadWallpaper` | Read your current wallpaper, playback state and settings |
|
||||
| `ControlPlayback` | Start, stop, skip and replace your wallpaper |
|
||||
| `DesktopOverlay` | Draw content on top of your desktop wallpaper |
|
||||
| `SettingsUi` | Add pages to the LiveWall settings window |
|
||||
| `Commands` | Add commands to LiveWall |
|
||||
| `TrayMenu` | Add entries to the LiveWall tray menu |
|
||||
| `Hotkeys` | Register system-wide keyboard shortcuts |
|
||||
| `FileSystem` | Read and write files on your computer |
|
||||
| `Network` | Connect to the internet |
|
||||
| `Renderer` | Take over wallpaper playback with its own renderer |
|
||||
| `Filters` | Rewrite or block wallpaper changes |
|
||||
| `All` | Union of all of the above |
|
||||
|
||||
`PluginPermissionInfo.Describe(PluginPermission)` returns the text for one flag;
|
||||
`PluginPermissionInfo.Expand(PluginPermission)` returns `(Permission, Description)` pairs — this is
|
||||
what the warning dialog iterates.
|
||||
|
||||
> Permissions are **declarations**, not a sandbox. The host enforces them where it can (it will refuse
|
||||
> to register an overlay without `DesktopOverlay`, or to change the wallpaper without
|
||||
> `ControlPlayback`), but nothing stops a determined plugin from calling Win32 directly. They exist so
|
||||
> the user knows what they are agreeing to — see [security.md](security.md).
|
||||
|
||||
---
|
||||
|
||||
## `ILiveWallPlugin`
|
||||
|
||||
```csharp
|
||||
public interface ILiveWallPlugin
|
||||
{
|
||||
void Initialize(IPluginContext context);
|
||||
void Shutdown();
|
||||
}
|
||||
```
|
||||
|
||||
* One public parameterless class per assembly, named by `"type"` in `plugin.json`.
|
||||
* `Initialize` runs on the WPF UI thread. Register extensions and subscribe to events here; do not
|
||||
block (no long `Thread.Sleep`, no `.Result`), and start expensive work on a background task.
|
||||
* Any exception escaping `Initialize` marks the plugin **Failed**; it is never retried automatically
|
||||
and the message is shown in the Plugins tab.
|
||||
* `Shutdown` may run on any thread during unload/exit. It must not block for long and must not throw.
|
||||
|
||||
## `IPluginContext`
|
||||
|
||||
| Member | Type | Notes |
|
||||
|---|---|---|
|
||||
| `Info` | `PluginInfo` | Identity, paths, hash, verification flag |
|
||||
| `Log` | `IPluginLog` | Prefixed with your plugin id, written to LiveWall's log |
|
||||
| `Storage` | `IPluginStorage` | Per-plugin JSON key/value store |
|
||||
| `Settings` | `ISettingsSnapshot` | Read-only settings snapshot (a fresh one each get) |
|
||||
| `Host` | `IHostActions` | Actions in the host |
|
||||
| `Events` | `IPluginEvents` | Subscribe / publish |
|
||||
| `Extensions` | `IPluginRegistrar` | Register overlays, pages, commands, filters, renderers, hotkeys |
|
||||
| `ShutdownToken` | `CancellationToken` | Cancelled when the plugin is being unloaded |
|
||||
|
||||
## `PluginInfo`
|
||||
|
||||
| Member | Type | Notes |
|
||||
|---|---|---|
|
||||
| `Id`, `Name`, `Version`, `Author`, `Description`, `Homepage` | `string` | From `plugin.json` |
|
||||
| `ApiVersion` | `string` | Version the plugin declared (normalised to `"1.0"`) |
|
||||
| `Permissions` | `PluginPermission` | Parsed from the manifest |
|
||||
| `Folder` | `string` | Absolute plugin folder |
|
||||
| `AssemblyPath` | `string` | Absolute path of the entry assembly |
|
||||
| `Sha256` | `string` | Lower-case hex SHA-256 of the assembly |
|
||||
| `Verified` | `bool` | `true` when this exact hash was approved on this machine |
|
||||
|
||||
## `IPluginLog`
|
||||
|
||||
```csharp
|
||||
void Debug(string message);
|
||||
void Info(string message);
|
||||
void Warn(string message);
|
||||
void Error(string message, Exception? exception = null);
|
||||
```
|
||||
|
||||
Lines land in `%APPDATA%\LiveWall\logs\livewall-YYYYMMDD.log` as
|
||||
`[com.example.clock] your message`. `Debug` only writes when LiveWall's debug logging is on.
|
||||
|
||||
## `IPluginStorage`
|
||||
|
||||
| Member | Notes |
|
||||
|---|---|
|
||||
| `string DataFolder` | `%APPDATA%\LiveWall\plugin-data\<id>\`, created on demand |
|
||||
| `T? Get<T>(string key, T? fallback = default)` | Deserialised with `System.Text.Json` |
|
||||
| `void Set<T>(string key, T value)` | Buffered; flushed automatically (debounced) and on shutdown |
|
||||
| `bool Remove(string key)` | Removes one key |
|
||||
| `IReadOnlyCollection<string> Keys` | All keys in the store |
|
||||
| `string PathFor(string fileName)` | Absolute path inside `DataFolder`; parent dirs created |
|
||||
| `void Flush()` | Force a write now |
|
||||
|
||||
The store is one JSON object per plugin (`storage.json`). Concurrent `Set` calls from different
|
||||
threads are safe. Keep it small (it is rewritten wholesale).
|
||||
|
||||
## `ISettingsSnapshot`
|
||||
|
||||
| Member | Type | Notes |
|
||||
|---|---|---|
|
||||
| `SnapshotId` | `Guid` | Changes on every snapshot |
|
||||
| `SourceKind` | `string` | `Video`, `AnimatedGif`, `Image`, `YouTube`, `WebPage` |
|
||||
| `SourceLocation` | `string` | File path or URL |
|
||||
| `Muted` | `bool` | |
|
||||
| `Volume` | `double` | 0–100 |
|
||||
| `Speed` | `double` | 0.25–4 |
|
||||
| `Loop` | `bool` | |
|
||||
| `InteractiveMode` | `bool` | `true` when the wallpaper accepts clicks |
|
||||
| `Paused` | `bool` | |
|
||||
| `DimPercent` | `int` | 0–100 |
|
||||
| `DimColor` | `string` | `#RRGGBB` |
|
||||
| `DisplayDevices` | `IReadOnlyList<string>` | Device names, primary first |
|
||||
|
||||
## `WallpaperState`
|
||||
|
||||
`Kind`, `Source`, `Title`, `IsPlaying`, `IsPaused`, `IsInteractive`, `SuppliedByPlugin`,
|
||||
`RendererName`. This is `IHostActions.Current`.
|
||||
|
||||
## `IHostActions`
|
||||
|
||||
| Member | Permission | Behaviour |
|
||||
|---|---|---|
|
||||
| `WallpaperState Current { get; }` | `ReadWallpaper` | `Kind`/`Source` empty when unknown |
|
||||
| `void SetWallpaper(WallpaperRequest)` | `ControlPlayback` | Routes through the normal filter chain then reloads |
|
||||
| `void Play()` | `ControlPlayback` | |
|
||||
| `void Pause()` | `ControlPlayback` | |
|
||||
| `void TogglePause()` | `ControlPlayback` | |
|
||||
| `void NextWallpaper()` | `ControlPlayback` | Next playlist entry |
|
||||
| `void ReloadWallpaper()` | `ControlPlayback` | Re-applies the current source |
|
||||
| `void Reattach()` | — | Re-parents the wallpaper window to WorkerW |
|
||||
| `void OpenSettings()` | — | Shows the settings window |
|
||||
| `void SetStatus(string)` | — | Status line text |
|
||||
| `void ShowNotification(string, string)` | — | Balloon, respects the user's notification setting |
|
||||
| `void SetOverlayVisible(bool)` | `DesktopOverlay` | Shows/hides **this plugin's** overlays |
|
||||
|
||||
Calls the plugin lacks permission for are logged and ignored — they never throw.
|
||||
|
||||
## `WallpaperRequest`
|
||||
|
||||
```csharp
|
||||
public sealed class WallpaperRequest
|
||||
{
|
||||
public string Kind { get; set; } = "Custom"; // Video | AnimatedGif | Image | YouTube | WebPage | Custom
|
||||
public string Source { get; set; } = ""; // path, URL, or anything your renderer understands
|
||||
public string Title { get; set; } = "";
|
||||
public Dictionary<string, string> Metadata { get; } // free-form, survives filters, not persisted
|
||||
public WallpaperRequest Clone();
|
||||
}
|
||||
```
|
||||
|
||||
`Metadata` keys are case-insensitive. Use it to hand parameters to your own renderer.
|
||||
|
||||
---
|
||||
|
||||
## Extension points
|
||||
|
||||
### `IPluginRegistrar`
|
||||
|
||||
```csharp
|
||||
void AddOverlay(IWallpaperOverlay overlay); // DesktopOverlay
|
||||
void RemoveOverlay(IWallpaperOverlay overlay);
|
||||
void AddSettingsPage(IPluginSettingsPage page); // SettingsUi
|
||||
void AddCommand(PluginCommand command); // Commands / TrayMenu
|
||||
void AddFilter(IWallpaperFilter filter); // Filters
|
||||
void AddRenderer(IWallpaperRendererPlugin renderer); // Renderer
|
||||
void AddHotkey(PluginHotkey hotkey); // Hotkeys
|
||||
```
|
||||
|
||||
Registering without the matching permission logs a warning and does nothing.
|
||||
|
||||
### `IWallpaperOverlay`
|
||||
|
||||
```csharp
|
||||
string Name { get; }
|
||||
FrameworkElement CreateView(OverlayContext context);
|
||||
void Update(OverlayState state);
|
||||
void OnVisibilityChanged(bool visible) { } // default: do nothing
|
||||
```
|
||||
|
||||
* `CreateView` is called once per display, on the UI thread, when the overlay is attached.
|
||||
* The element is stretched to the display's DIP size and **is not hit-testable by default** — a
|
||||
click on your overlay falls through to the desktop. If you need clicks, use `InteractiveMode` plus a
|
||||
renderer instead; the overlay layer is deliberately click-through.
|
||||
* Keep it cheap: it repaints whenever anything below it repaints. Avoid `Effect`s on large elements,
|
||||
prefer text and simple shapes, and stop timers in `OnVisibilityChanged(false)`.
|
||||
|
||||
`OverlayContext`: `DisplayIndex`, `DeviceName`, `Width`, `Height` (DIP), `DpiScale`, `Primary`.
|
||||
|
||||
`OverlayState`: `Kind`, `Source`, `Title`, `IsPlaying`, `IsPaused`, `IsInteractive`, `TimestampUtc`.
|
||||
|
||||
### `IPluginSettingsPage`
|
||||
|
||||
```csharp
|
||||
string Title { get; }
|
||||
string Description { get; }
|
||||
FrameworkElement CreateView();
|
||||
```
|
||||
|
||||
Pages appear under **Plugins ▸ Installed plugins ▸ Settings**. Several pages = several tabs in one
|
||||
window. `CreateView` is called when the window opens, so build fresh controls and read your state
|
||||
there. The element tree is LiveWall's; if it throws, the message is shown in the page instead of
|
||||
crashing the app.
|
||||
|
||||
Style your controls like the rest of the app — `(Brush)Application.Current.Resources["TextBrush"]`,
|
||||
`["TextDim"]`, `["BgField"]`, `["AccentBrush"]`, `["DangerBrush"]`, and the `Card`, `Hint`,
|
||||
`SectionTitle`, `AccentButton`, `GhostButton`, `TinyButton` styles. They are always present.
|
||||
|
||||
### `PluginCommand`
|
||||
|
||||
```csharp
|
||||
new PluginCommand("id", "Human title", ctx => { /* ... */ })
|
||||
{
|
||||
Category = "Clock", // optional grouping label
|
||||
CanExecute = () => true // optional; evaluated when the menu opens
|
||||
};
|
||||
```
|
||||
|
||||
`ctx.Plugin` is the plugin's `IPluginContext`, `ctx.Host` is `IHostActions`. Commands appear in
|
||||
**Plugins ▸ Plugin commands** with a **Run** button; those with the `TrayMenu` permission also appear
|
||||
in the tray menu under **Plugins**.
|
||||
|
||||
### `PluginHotkey`
|
||||
|
||||
```csharp
|
||||
new PluginHotkey("clock.toggle24", "Ctrl+Alt+F9", () => { /* ... */ })
|
||||
{
|
||||
Description = "Clock: toggle 12/24 hour" // tooltip text in the hotkey list
|
||||
};
|
||||
```
|
||||
|
||||
Gestures accept the same syntax as LiveWall's own hotkeys: modifiers `Ctrl`/`Control`, `Alt`,
|
||||
`Shift`, `Win` plus one key (`F1`–`F24`, letters, digits, `Space`, `Pause`, `PrintScreen`,
|
||||
`Insert`/`Delete`/`Home`/`End`/`PageUp`/`PageDown`, arrows, numpad keys). Registration failure (the
|
||||
combination is taken) is logged and reported — LiveWall's own hotkeys always win.
|
||||
|
||||
### `IWallpaperFilter`
|
||||
|
||||
```csharp
|
||||
int Priority { get; } // higher runs first
|
||||
WallpaperRequest Apply(WallpaperRequest request, IHostActions host);
|
||||
```
|
||||
|
||||
Filters run in the UI thread **before** renderer selection, in descending `Priority` order (ties run in
|
||||
registration order). Return the (possibly new) request; returning the argument unchanged leaves it
|
||||
alone. Throwing removes your filter from that pass and is logged — other filters still run.
|
||||
|
||||
A filter can therefore: rewrite `Source` (resolve `ytsearch:` links, expand `%DATE%`), add
|
||||
`Metadata`, or veto by returning the current state (`host.Current`).
|
||||
|
||||
### `IWallpaperRendererPlugin` / `IWallpaperRenderer`
|
||||
|
||||
```csharp
|
||||
public interface IWallpaperRendererPlugin
|
||||
{
|
||||
string Name { get; }
|
||||
int Priority { get; } // higher wins among plugins
|
||||
bool CanRender(WallpaperRequest request);
|
||||
IWallpaperRenderer Create();
|
||||
}
|
||||
|
||||
public interface IWallpaperRenderer : IDisposable
|
||||
{
|
||||
FrameworkElement View { get; } // the wallpaper visual, keep it opaque
|
||||
void Load(WallpaperRequest request); // throw to report a failure
|
||||
void Play();
|
||||
void Pause();
|
||||
void Unload();
|
||||
void ApplyPlayback(bool muted, double volume, double speed);
|
||||
event EventHandler<string>? Failed;
|
||||
event EventHandler? Ended;
|
||||
}
|
||||
```
|
||||
|
||||
LiveWall asks every renderer, highest `Priority` first, whether it `CanRender`s the request. The first
|
||||
`true` wins and the built-in players (video/GIF/YouTube/web) are skipped — so a plugin can take over
|
||||
sources LiveWall would otherwise handle. Then `Create()` is called once, `Load(request)` once per
|
||||
source, and `View` is placed in the wallpaper window. `Failed`/`Ended` surface in the status line and
|
||||
the log. `Dispose` is called when the renderer is replaced or the plugin unloads.
|
||||
|
||||
Remember: the wallpaper window is behind the desktop icons and **not** interactive unless
|
||||
`InteractiveMode` is on.
|
||||
|
||||
---
|
||||
|
||||
## Events (`Events.cs`)
|
||||
|
||||
| Event | Payload |
|
||||
|---|---|
|
||||
| `AppStartedEvent` | `HostVersion`, `PluginApiVersion` |
|
||||
| `AppStoppingEvent` | `Reason` |
|
||||
| `WallpaperChangedEvent` | `Kind`, `Source`, `RendererName`, `FromPlugin` |
|
||||
| `PlaybackChangedEvent` | `IsPlaying`, `IsPaused`, `Reason` |
|
||||
| `InteractionChangedEvent` | `Interactive` |
|
||||
| `SettingsChangedEvent` | `Property`, `Snapshot` |
|
||||
| `DisplayChangedEvent` | `Devices`, `Reattached` |
|
||||
| `SessionChangedEvent` | `Locked`, `OnBattery`, `RemoteSession` |
|
||||
| `HostErrorEvent` | `Message`, `Source` |
|
||||
| `PluginMessageEvent` | `FromPluginId`, `Topic`, `Payload` (string) |
|
||||
|
||||
```csharp
|
||||
IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class;
|
||||
void Publish<TEvent>(TEvent payload) where TEvent : class;
|
||||
```
|
||||
|
||||
* Handlers always run on the WPF UI thread. Never block in one — bounce work to `Task.Run` if needed.
|
||||
* Always keep the returned `IDisposable` and dispose it in `Shutdown`.
|
||||
* `Publish` anything you like: it is a type-based bus, so you can define your own event classes for
|
||||
plugin-to-plugin messaging (`PluginMessageEvent` is the conventional envelope).
|
||||
* Publishing a host event type is allowed but only reaches other plugins — the host does not re-apply
|
||||
it.
|
||||
Reference in New Issue
Block a user