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

342 lines
12 KiB
C#

using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Windows.Threading;
using LiveWall.Sdk;
namespace ClockOverlayPlugin;
/// <summary>
/// Example plugin that touches every extension point, in the order a real plugin would use them:
///
/// 1. a desktop overlay (a clock, drawn above the wallpaper),
/// 2. a settings page (cursor colour, 12/24 h, show wallpaper name),
/// 3. a command ("Say hello") that appears in the app and the tray menu,
/// 4. a wallpaper filter that can veto changes while "pin wallpaper" is on,
/// 5. a global hotkey (Ctrl+Alt+F9),
/// 6. storage (colour + flags survive restarts),
/// 7. event subscriptions (wallpaper changes, playback changes).
/// </summary>
public sealed class ClockPlugin : ILiveWallPlugin
{
private IPluginContext? _context;
private IDisposable? _wallpaperSub;
private IDisposable? _playbackSub;
private string _colour = "#FFFFFF";
private bool _use24Hour = true;
private bool _showWallpaperName = true;
private bool _pinWallpaper;
public void Initialize(IPluginContext context)
{
_context = context;
var log = context.Log;
log.Info($"Clock plugin starting (plugin API {PluginApi.Version}, host {PluginApi.HostVersion})");
// ---- 6. persisted settings -------------------------------------------------
_colour = context.Storage.Get("colour", "#FFFFFF") ?? "#FFFFFF";
_use24Hour = context.Storage.Get("use24Hour", true);
_showWallpaperName = context.Storage.Get("showWallpaperName", true);
_pinWallpaper = context.Storage.Get("pinWallpaper", false);
// ---- 1. overlay ------------------------------------------------------------
context.Extensions.AddOverlay(new ClockOverlay(this));
// ---- 2. settings page ------------------------------------------------------
context.Extensions.AddSettingsPage(new ClockSettingsPage(this, context));
// ---- 3. command ------------------------------------------------------------
context.Extensions.AddCommand(new PluginCommand("clock.hello", "Say hello from the clock plugin",
ctx =>
{
ctx.Host.ShowNotification("Clock plugin", "Hello! Your wallpaper is " + Describe(ctx.Host.Current));
ctx.Host.SetStatus("Clock plugin says hello");
})
{
Category = "Example",
CanExecute = () => _context != null
});
// ---- 4. wallpaper filter ---------------------------------------------------
context.Extensions.AddFilter(new PinWallpaperFilter(this));
// ---- 5. hotkey -------------------------------------------------------------
context.Extensions.AddHotkey(new PluginHotkey("clock.toggle24", "Ctrl+Alt+F9", () =>
{
_use24Hour = !_use24Hour;
Save();
_context?.Host.SetStatus(_use24Hour ? "Clock: 24 hour" : "Clock: 12 hour");
})
{
Description = "Clock: toggle 12/24 hour"
});
// ---- 7. events -------------------------------------------------------------
_wallpaperSub = context.Events.Subscribe<WallpaperChangedEvent>(e =>
context.Log.Info($"Wallpaper changed: {e.Kind} {e.Source} via {e.RendererName}"));
_playbackSub = context.Events.Subscribe<PlaybackChangedEvent>(e =>
context.Log.Debug($"Playback: playing={e.IsPlaying} paused={e.IsPaused}"));
context.Events.Subscribe<InteractionChangedEvent>(e =>
context.Log.Debug($"Interactive mode: {e.Interactive}"));
}
public void Shutdown()
{
_wallpaperSub?.Dispose();
_playbackSub?.Dispose();
_context = null;
}
// ------------------------------------------------------------------ used by the overlay / page
public string Colour => _colour;
public bool Use24Hour => _use24Hour;
public bool ShowWallpaperName => _showWallpaperName;
public bool PinWallpaper => _pinWallpaper;
public void SetColour(string colour)
{
_colour = colour;
Save();
Refresh();
}
public void SetUse24Hour(bool value)
{
_use24Hour = value;
Save();
Refresh();
}
public void SetShowWallpaperName(bool value)
{
_showWallpaperName = value;
Save();
Refresh();
}
public void SetPinWallpaper(bool value)
{
_pinWallpaper = value;
Save();
}
private void Save()
{
var storage = _context?.Storage;
if (storage == null) return;
storage.Set("colour", _colour);
storage.Set("use24Hour", _use24Hour);
storage.Set("showWallpaperName", _showWallpaperName);
storage.Set("pinWallpaper", _pinWallpaper);
}
private void Refresh() => _overlay?.Refresh();
private ClockOverlay? _overlay;
private void AttachOverlay(ClockOverlay overlay) => _overlay = overlay;
private static string Describe(WallpaperState state)
=> string.IsNullOrEmpty(state.Kind) ? "(unknown)" : $"{state.Kind}: {state.Source}";
// ------------------------------------------------------------------ the overlay itself
private sealed class ClockOverlay : IWallpaperOverlay
{
private readonly ClockPlugin _plugin;
private TextBlock? _clock;
private TextBlock? _source;
private DispatcherTimer? _timer;
public ClockOverlay(ClockPlugin plugin)
{
_plugin = plugin;
plugin.AttachOverlay(this);
}
public string Name => "Clock";
public FrameworkElement CreateView(OverlayContext context)
{
var panel = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 24, 32, 0),
IsHitTestVisible = false
};
_clock = new TextBlock
{
FontFamily = new FontFamily("Segoe UI Light"),
FontSize = 46,
Foreground = ParseBrush(_plugin.Colour),
TextAlignment = TextAlignment.Right,
Effect = new DropShadowEffect { BlurRadius = 18, ShadowDepth = 0, Opacity = 0.85, Color = Colors.Black }
};
_source = new TextBlock
{
FontSize = 12,
Foreground = new SolidColorBrush(Color.FromArgb(180, 255, 255, 255)),
TextAlignment = TextAlignment.Right,
Margin = new Thickness(0, 2, 2, 0),
Visibility = _plugin.ShowWallpaperName ? Visibility.Visible : Visibility.Collapsed
};
panel.Children.Add(_clock);
panel.Children.Add(_source);
_timer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromSeconds(1) };
_timer.Tick += (_, _) => Tick();
_timer.Start();
Tick();
return panel;
}
public void Update(OverlayState state)
{
if (_source != null && _plugin.ShowWallpaperName)
{
_source.Text = string.IsNullOrEmpty(state.Kind) ? "" : $"{state.Kind} · {System.IO.Path.GetFileName(state.Source)}";
_source.Visibility = Visibility.Visible;
}
else if (_source != null)
{
_source.Visibility = Visibility.Collapsed;
}
}
public void OnVisibilityChanged(bool visible)
{
if (visible) _timer?.Start();
else _timer?.Stop();
}
public void Refresh()
{
if (_source != null)
_source.Visibility = _plugin.ShowWallpaperName ? Visibility.Visible : Visibility.Collapsed;
Tick();
}
private void Tick()
{
if (_clock == null) return;
var now = DateTime.Now;
string format = _plugin.Use24Hour ? "HH:mm" : "h:mm tt";
_clock.Text = now.ToString(format, CultureInfo.CurrentCulture);
_clock.Foreground = ParseBrush(_plugin.Colour);
}
private static Brush ParseBrush(string hex)
{
try { return (Brush)new BrushConverter().ConvertFromString(hex)!; }
catch { return Brushes.White; }
}
}
// ------------------------------------------------------------------ the filter
private sealed class PinWallpaperFilter : IWallpaperFilter
{
private readonly ClockPlugin _plugin;
public PinWallpaperFilter(ClockPlugin plugin) => _plugin = plugin;
public int Priority => 0;
public WallpaperRequest Apply(WallpaperRequest request, IHostActions host)
{
if (!_plugin.PinWallpaper) return request;
// "Pin wallpaper" means: keep whatever is on screen, refuse the change.
host.SetStatus("Clock plugin: wallpaper changes are pinned");
return new WallpaperRequest
{
Kind = host.Current.Kind,
Source = host.Current.Source,
Title = "pinned"
};
}
}
// ------------------------------------------------------------------ the settings page
private sealed class ClockSettingsPage : IPluginSettingsPage
{
private readonly ClockPlugin _plugin;
private readonly IPluginContext _context;
public ClockSettingsPage(ClockPlugin plugin, IPluginContext context)
{
_plugin = plugin;
_context = context;
}
public string Title => "Clock";
public string Description => "Position, colour and format of the clock overlay.";
public FrameworkElement CreateView()
{
var panel = new StackPanel();
panel.Children.Add(new TextBlock
{
Text = "Colour (any WPF colour syntax)",
Margin = new Thickness(0, 0, 0, 4),
Foreground = new SolidColorBrush(Color.FromRgb(0x8C, 0x95, 0xA5))
});
var colourBox = new TextBox { Text = _plugin.Colour, Width = 160, HorizontalAlignment = HorizontalAlignment.Left };
colourBox.TextChanged += (_, _) => _plugin.SetColour(colourBox.Text);
panel.Children.Add(colourBox);
var use24 = new CheckBox { Content = "24 hour clock", IsChecked = _plugin.Use24Hour, Margin = new Thickness(0, 12, 0, 0) };
use24.Checked += (_, _) => _plugin.SetUse24Hour(true);
use24.Unchecked += (_, _) => _plugin.SetUse24Hour(false);
panel.Children.Add(use24);
var showName = new CheckBox
{
Content = "Show the wallpaper name under the clock",
IsChecked = _plugin.ShowWallpaperName,
Margin = new Thickness(0, 8, 0, 0)
};
showName.Checked += (_, _) => _plugin.SetShowWallpaperName(true);
showName.Unchecked += (_, _) => _plugin.SetShowWallpaperName(false);
panel.Children.Add(showName);
var pin = new CheckBox
{
Content = "Pin the wallpaper (this plugin's filter blocks changes)",
IsChecked = _plugin.PinWallpaper,
Margin = new Thickness(0, 8, 0, 0)
};
pin.Checked += (_, _) => _plugin.SetPinWallpaper(true);
pin.Unchecked += (_, _) => _plugin.SetPinWallpaper(false);
panel.Children.Add(pin);
panel.Children.Add(new TextBlock
{
Text = "Storage file: " + _context.Storage.PathFor("storage.json"),
Margin = new Thickness(0, 16, 0, 0),
FontSize = 11,
TextWrapping = TextWrapping.Wrap,
Foreground = new SolidColorBrush(Color.FromRgb(0x8C, 0x95, 0xA5))
});
return panel;
}
}
}