373 lines
10 KiB
Markdown
373 lines
10 KiB
Markdown
# Recipes
|
|
|
|
Copy, adapt, ship. Everything here compiles against `LiveWall.Sdk` 1.0; the complete working example
|
|
is `samples/ClockOverlayPlugin/`.
|
|
|
|
---
|
|
|
|
## A minimal plugin that just does something
|
|
|
|
```csharp
|
|
using LiveWall.Sdk;
|
|
|
|
public sealed class PingPlugin : ILiveWallPlugin
|
|
{
|
|
public void Initialize(IPluginContext context)
|
|
{
|
|
context.Log.Info("ping plugin ready");
|
|
|
|
context.Extensions.AddCommand(new PluginCommand("ping.now", "Ping: notify me",
|
|
ctx => ctx.Host.ShowNotification("Ping", "Still here.")));
|
|
}
|
|
|
|
public void Shutdown() { }
|
|
}
|
|
```
|
|
|
|
```json
|
|
{ "id": "com.example.ping", "name": "Ping", "version": "1.0.0", "apiVersion": "1.0",
|
|
"entry": "Ping.dll", "type": "PingPlugin", "minHostVersion": "1.8.0",
|
|
"permissions": ["Commands"] }
|
|
```
|
|
|
|
---
|
|
|
|
## An overlay that shows the wallpaper's name
|
|
|
|
```csharp
|
|
public sealed class NameOverlay : IWallpaperOverlay
|
|
{
|
|
private TextBlock? _text;
|
|
|
|
public string Name => "Now playing";
|
|
|
|
public FrameworkElement CreateView(OverlayContext context)
|
|
{
|
|
_text = new TextBlock
|
|
{
|
|
Text = "",
|
|
FontSize = 13,
|
|
Foreground = new SolidColorBrush(Color.FromArgb(170, 255, 255, 255)),
|
|
HorizontalAlignment = HorizontalAlignment.Left,
|
|
VerticalAlignment = VerticalAlignment.Bottom,
|
|
Margin = new Thickness(24, 0, 0, 20)
|
|
};
|
|
return _text;
|
|
}
|
|
|
|
public void Update(OverlayState state)
|
|
{
|
|
if (_text == null) return;
|
|
_text.Text = string.IsNullOrEmpty(state.Source)
|
|
? ""
|
|
: $"{state.Kind} · {System.IO.Path.GetFileName(state.Source)}" +
|
|
(state.IsPaused ? " (paused)" : "");
|
|
}
|
|
}
|
|
```
|
|
|
|
`Update` is called on every wallpaper/playback change — no polling needed.
|
|
|
|
---
|
|
|
|
## An animated overlay (visualiser-ish) with a timer that behaves
|
|
|
|
```csharp
|
|
public sealed class PulseOverlay : IWallpaperOverlay
|
|
{
|
|
private readonly List<Rectangle> _bars = new();
|
|
private DispatcherTimer? _timer;
|
|
private readonly Random _random = new();
|
|
|
|
public string Name => "Pulse";
|
|
|
|
public FrameworkElement CreateView(OverlayContext context)
|
|
{
|
|
var panel = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
VerticalAlignment = VerticalAlignment.Bottom,
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 0, 32),
|
|
Height = 120
|
|
};
|
|
|
|
for (int i = 0; i < 32; i++)
|
|
{
|
|
var bar = new Rectangle
|
|
{
|
|
Width = 6,
|
|
Height = 10,
|
|
Margin = new Thickness(2, 0, 2, 0),
|
|
VerticalAlignment = VerticalAlignment.Bottom,
|
|
Fill = new SolidColorBrush(Color.FromArgb(140, 0x4C, 0xC2, 0xFF))
|
|
};
|
|
_bars.Add(bar);
|
|
panel.Children.Add(bar);
|
|
}
|
|
|
|
_timer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(80) };
|
|
_timer.Tick += (_, _) =>
|
|
{
|
|
foreach (var bar in _bars)
|
|
bar.Height = 8 + _random.Next(105);
|
|
};
|
|
_timer.Start();
|
|
|
|
return panel;
|
|
}
|
|
|
|
public void Update(OverlayState state) { }
|
|
|
|
public void OnVisibilityChanged(bool visible)
|
|
{
|
|
if (visible) _timer?.Start();
|
|
else _timer?.Stop();
|
|
}
|
|
}
|
|
```
|
|
|
|
Stopping the timer when hidden is what keeps a wallpapered machine cool while a game is fullscreen.
|
|
|
|
---
|
|
|
|
## A filter that resolves shorthand and env vars
|
|
|
|
```csharp
|
|
public sealed class ResolveFilter : IWallpaperFilter
|
|
{
|
|
public int Priority => 100; // run before other plugins' filters
|
|
|
|
public WallpaperRequest Apply(WallpaperRequest request, IHostActions host)
|
|
{
|
|
var source = request.Source;
|
|
|
|
if (source.StartsWith("yt:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var query = source[3..];
|
|
request.Source = "https://www.youtube.com/results?search_query=" + Uri.EscapeDataString(query);
|
|
request.Kind = "YouTube";
|
|
}
|
|
else if (source.StartsWith("home:"))
|
|
{
|
|
request.Source = Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), source[5..]);
|
|
request.Kind = "Video";
|
|
}
|
|
|
|
return request;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## A filter that pins the wallpaper
|
|
|
|
```csharp
|
|
public sealed class PinFilter : IWallpaperFilter
|
|
{
|
|
private readonly IHostActions _host;
|
|
public bool Pinned { get; set; }
|
|
|
|
public PinFilter(IHostActions host) => _host = host;
|
|
|
|
public int Priority => 1000;
|
|
|
|
public WallpaperRequest Apply(WallpaperRequest request, IHostActions host)
|
|
{
|
|
if (!Pinned) return request;
|
|
|
|
var current = host.Current;
|
|
return new WallpaperRequest { Kind = current.Kind, Source = current.Source, Title = "pinned" };
|
|
}
|
|
}
|
|
```
|
|
|
|
Toggle `Pinned` from a command or a settings page; the wallpaper stops changing even for playlists.
|
|
|
|
---
|
|
|
|
## A custom renderer (clock/simulation style)
|
|
|
|
```csharp
|
|
public sealed class ShaderRendererPlugin : IWallpaperRendererPlugin
|
|
{
|
|
public string Name => "Example shader";
|
|
public int Priority => 0;
|
|
|
|
public bool CanRender(WallpaperRequest request)
|
|
=> request.Kind.Equals("Custom", StringComparison.OrdinalIgnoreCase)
|
|
&& request.Source.StartsWith("example-scene:", StringComparison.OrdinalIgnoreCase);
|
|
|
|
public IWallpaperRenderer Create() => new ShaderRenderer();
|
|
}
|
|
|
|
public sealed class ShaderRenderer : IWallpaperRenderer
|
|
{
|
|
private readonly Grid _root = new();
|
|
private DispatcherTimer? _timer;
|
|
private double _t;
|
|
|
|
public event EventHandler<string>? Failed;
|
|
public event EventHandler? Ended;
|
|
|
|
public FrameworkElement View => _root;
|
|
|
|
public void Load(WallpaperRequest request)
|
|
{
|
|
var name = request.Source["example-scene:".Length..];
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
throw new ArgumentException("no scene name in " + request.Source);
|
|
|
|
_root.Background = new SolidColorBrush(Color.FromRgb(8, 10, 16)); // stay opaque
|
|
_root.Children.Clear();
|
|
_root.Children.Add(new TextBlock
|
|
{
|
|
Text = "scene: " + name,
|
|
Foreground = Brushes.White,
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center
|
|
});
|
|
|
|
_timer = new DispatcherTimer(DispatcherPriority.Render) { Interval = TimeSpan.FromMilliseconds(33) };
|
|
_timer.Tick += (_, _) => _t += 0.033;
|
|
_timer.Start();
|
|
}
|
|
|
|
public void Play() => _timer?.Start();
|
|
public void Pause() => _timer?.Stop();
|
|
public void Unload() { _timer?.Stop(); _timer = null; }
|
|
public void ApplyPlayback(bool muted, double volume, double speed) { /* use them if you like */ }
|
|
public void Dispose() => Unload();
|
|
}
|
|
```
|
|
|
|
Declare `"permissions": ["Renderer", "ControlPlayback"]` and the plugin can be selected by setting
|
|
`Kind = "Custom"`, `Source = "example-scene:rain"` via `host.SetWallpaper(...)`.
|
|
|
|
---
|
|
|
|
## A settings page with LiveWall styling
|
|
|
|
```csharp
|
|
public sealed class MyPage : IPluginSettingsPage
|
|
{
|
|
private readonly IPluginContext _context;
|
|
public MyPage(IPluginContext context) => _context = context;
|
|
|
|
public string Title => "Example";
|
|
public string Description => "Where the example settings live.";
|
|
|
|
public FrameworkElement CreateView()
|
|
{
|
|
var res = Application.Current.Resources;
|
|
|
|
var panel = new StackPanel();
|
|
panel.Children.Add(new TextBlock
|
|
{
|
|
Text = "Scene",
|
|
Foreground = (Brush)res["TextDim"],
|
|
Margin = new Thickness(0, 0, 0, 4)
|
|
});
|
|
|
|
var combo = new ComboBox { Width = 200, HorizontalAlignment = HorizontalAlignment.Left };
|
|
combo.ItemsSource = new[] { "rain", "snow", "stars" };
|
|
combo.SelectedItem = _context.Storage.Get("scene", "rain");
|
|
combo.SelectionChanged += (_, _) =>
|
|
{
|
|
_context.Storage.Set("scene", combo.SelectedItem as string ?? "rain");
|
|
_context.Host.SetStatus("Example: scene = " + combo.SelectedItem);
|
|
};
|
|
panel.Children.Add(combo);
|
|
|
|
var button = new Button
|
|
{
|
|
Content = "Apply now",
|
|
Style = (Style)res["AccentButton"],
|
|
HorizontalAlignment = HorizontalAlignment.Left,
|
|
Margin = new Thickness(0, 14, 0, 0)
|
|
};
|
|
button.Click += (_, _) => _context.Host.SetWallpaper(new WallpaperRequest
|
|
{
|
|
Kind = "Custom",
|
|
Source = "example-scene:" + _context.Storage.Get("scene", "rain")
|
|
});
|
|
panel.Children.Add(button);
|
|
|
|
return panel;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Reacting to events without leaking
|
|
|
|
```csharp
|
|
private readonly List<IDisposable> _subs = new();
|
|
|
|
public void Initialize(IPluginContext context)
|
|
{
|
|
_subs.Add(context.Events.Subscribe<WallpaperChangedEvent>(e =>
|
|
context.Log.Info($"new wallpaper: {e.Kind} {System.IO.Path.GetFileName(e.Source)}")));
|
|
|
|
_subs.Add(context.Events.Subscribe<SessionChangedEvent>(e =>
|
|
{
|
|
if (e.Locked) context.Host.Pause(); // cheap power saving
|
|
else context.Host.Play();
|
|
}));
|
|
|
|
_subs.Add(context.Events.Subscribe<SettingsChangedEvent>(e =>
|
|
{
|
|
var s = e.Snapshot; // read-only view of the new settings
|
|
context.Log.Debug($"settings changed ({e.Property}): volume {s.Volume}, muted {s.Muted}");
|
|
}));
|
|
}
|
|
|
|
public void Shutdown()
|
|
{
|
|
foreach (var sub in _subs) sub.Dispose();
|
|
_subs.Clear();
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Talking to another plugin
|
|
|
|
```csharp
|
|
// sender
|
|
context.Events.Publish(new PluginMessageEvent
|
|
{
|
|
FromPluginId = context.Info.Id,
|
|
Topic = "weather.now",
|
|
Payload = """{"temp":18.5,"sky":"rain"}"""
|
|
});
|
|
|
|
// listener (another plugin)
|
|
context.Events.Subscribe<PluginMessageEvent>(m =>
|
|
{
|
|
if (m.Topic == "weather.now") context.Log.Info($"weather: {m.Payload}");
|
|
});
|
|
```
|
|
|
|
`PluginMessageEvent` is just a convention — the bus is type based, so any class you both reference
|
|
works. Ship the shared class in a small library, or keep to `PluginMessageEvent` to avoid one.
|
|
|
|
---
|
|
|
|
## Prefer to write it in the tray only?
|
|
|
|
```csharp
|
|
context.Extensions.AddCommand(new PluginCommand("quiet.toggle", "Mute wallpaper",
|
|
ctx => { if (ctx.Host.Current.IsPlaying) ctx.Host.Pause(); else ctx.Host.Play(); })
|
|
{
|
|
Category = "Audio",
|
|
CanExecute = () => true
|
|
});
|
|
```
|
|
|
|
With `"permissions": ["TrayMenu", "ControlPlayback", "ReadWallpaper"]` that is a complete, useful
|
|
plugin in twelve lines.
|