diff --git a/LanMountainDesktop.PluginsInstallHelper/LanMountainDesktop.PluginsInstallHelper.csproj b/LanMountainDesktop.PluginsInstallHelper/LanMountainDesktop.PluginsInstallHelper.csproj
new file mode 100644
index 0000000..924f51f
--- /dev/null
+++ b/LanMountainDesktop.PluginsInstallHelper/LanMountainDesktop.PluginsInstallHelper.csproj
@@ -0,0 +1,12 @@
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
diff --git a/LanMountainDesktop.PluginsInstallHelper/Program.cs b/LanMountainDesktop.PluginsInstallHelper/Program.cs
new file mode 100644
index 0000000..04677f1
--- /dev/null
+++ b/LanMountainDesktop.PluginsInstallHelper/Program.cs
@@ -0,0 +1,177 @@
+using System.IO.Compression;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+using LanMountainDesktop.PluginSdk;
+
+internal static class Program
+{
+ private static async Task Main(string[] args)
+ {
+ var result = new HelperResult();
+ string? resultPath = null;
+
+ try
+ {
+ var parsedArgs = ParseArgs(args);
+ if (!parsedArgs.TryGetValue("source", out var sourcePath) ||
+ !parsedArgs.TryGetValue("plugins-dir", out var pluginsDirectory) ||
+ !parsedArgs.TryGetValue("result", out resultPath) ||
+ string.IsNullOrWhiteSpace(sourcePath) ||
+ string.IsNullOrWhiteSpace(pluginsDirectory) ||
+ string.IsNullOrWhiteSpace(resultPath))
+ {
+ throw new InvalidOperationException("Required arguments: --source --plugins-dir --result .");
+ }
+
+ var fullSourcePath = Path.GetFullPath(sourcePath);
+ var fullPluginsDirectory = Path.GetFullPath(pluginsDirectory);
+ resultPath = Path.GetFullPath(resultPath);
+
+ if (!File.Exists(fullSourcePath))
+ {
+ throw new FileNotFoundException($"Plugin package '{fullSourcePath}' was not found.", fullSourcePath);
+ }
+
+ var manifest = ReadManifestFromPackage(fullSourcePath);
+ Directory.CreateDirectory(fullPluginsDirectory);
+ RemoveExistingPluginPackages(fullPluginsDirectory, manifest.Id);
+
+ var destinationPath = Path.Combine(fullPluginsDirectory, BuildInstalledPackageFileName(manifest.Id));
+ File.Copy(fullSourcePath, destinationPath, overwrite: true);
+
+ result = new HelperResult
+ {
+ Success = true,
+ InstalledPackagePath = destinationPath,
+ ManifestId = manifest.Id,
+ ManifestName = manifest.Name
+ };
+ }
+ catch (Exception ex)
+ {
+ result = new HelperResult
+ {
+ Success = false,
+ ErrorMessage = ex.Message
+ };
+ }
+
+ if (!string.IsNullOrWhiteSpace(resultPath))
+ {
+ var resultDirectory = Path.GetDirectoryName(resultPath);
+ if (!string.IsNullOrWhiteSpace(resultDirectory))
+ {
+ Directory.CreateDirectory(resultDirectory);
+ }
+
+ await File.WriteAllTextAsync(
+ resultPath,
+ JsonSerializer.Serialize(result, new JsonSerializerOptions
+ {
+ WriteIndented = true
+ }),
+ Encoding.UTF8);
+ }
+
+ return result.Success ? 0 : 1;
+ }
+
+ private static Dictionary ParseArgs(string[] args)
+ {
+ var values = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ for (var i = 0; i < args.Length; i++)
+ {
+ var current = args[i];
+ if (!current.StartsWith("--", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ var key = current[2..];
+ if (string.IsNullOrWhiteSpace(key) || i + 1 >= args.Length)
+ {
+ continue;
+ }
+
+ values[key] = args[++i];
+ }
+
+ return values;
+ }
+
+ private static PluginManifest ReadManifestFromPackage(string packagePath)
+ {
+ using var archive = ZipFile.OpenRead(packagePath);
+ var entries = archive.Entries
+ .Where(entry => string.Equals(entry.Name, PluginSdkInfo.ManifestFileName, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+
+ if (entries.Length == 0)
+ {
+ throw new InvalidOperationException(
+ $"Plugin package '{packagePath}' does not contain '{PluginSdkInfo.ManifestFileName}'.");
+ }
+
+ if (entries.Length > 1)
+ {
+ throw new InvalidOperationException(
+ $"Plugin package '{packagePath}' contains multiple '{PluginSdkInfo.ManifestFileName}' files.");
+ }
+
+ using var stream = entries[0].Open();
+ return PluginManifest.Load(stream, $"{packagePath}!/{entries[0].FullName}");
+ }
+
+ private static void RemoveExistingPluginPackages(string pluginsDirectory, string pluginId)
+ {
+ var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(pluginsDirectory), PluginSdkInfo.RuntimeDirectoryName));
+ foreach (var existingPackagePath in Directory
+ .EnumerateFiles(pluginsDirectory, "*" + PluginSdkInfo.PackageFileExtension, SearchOption.AllDirectories)
+ .Select(Path.GetFullPath)
+ .Where(path => !path.StartsWith(runtimeRootDirectory, StringComparison.OrdinalIgnoreCase)))
+ {
+ try
+ {
+ var existingManifest = ReadManifestFromPackage(existingPackagePath);
+ if (!string.Equals(existingManifest.Id, pluginId, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ File.Delete(existingPackagePath);
+ }
+ catch
+ {
+ // Ignore unrelated or malformed packages while replacing an install target.
+ }
+ }
+ }
+
+ private static string BuildInstalledPackageFileName(string pluginId)
+ {
+ var invalidChars = Path.GetInvalidFileNameChars();
+ var fileName = new string(pluginId.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
+ return fileName + PluginSdkInfo.PackageFileExtension;
+ }
+
+ private static string EnsureTrailingSeparator(string path)
+ {
+ return path.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)
+ ? path
+ : path + Path.DirectorySeparatorChar;
+ }
+
+ private sealed class HelperResult
+ {
+ public bool Success { get; init; }
+
+ public string? InstalledPackagePath { get; init; }
+
+ public string? ManifestId { get; init; }
+
+ public string? ManifestName { get; init; }
+
+ public string? ErrorMessage { get; init; }
+ }
+}
diff --git a/LanMountainDesktop.sln b/LanMountainDesktop.sln
index 243bd0e..efc0986 100644
--- a/LanMountainDesktop.sln
+++ b/LanMountainDesktop.sln
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanMountainDesktop.PluginPa
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanMountainDesktop.PluginSdk", "LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj", "{30A0F689-AACC-48C8-8BFE-BC7BFBA6CC55}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanMountainDesktop.PluginsInstallHelper", "LanMountainDesktop.PluginsInstallHelper\LanMountainDesktop.PluginsInstallHelper.csproj", "{5A20D5E9-BD27-4DD4-9F4F-97E0367DD2AC}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -33,5 +35,9 @@ Global
{30A0F689-AACC-48C8-8BFE-BC7BFBA6CC55}.Debug|Any CPU.Build.0 = Debug|Any CPU
{30A0F689-AACC-48C8-8BFE-BC7BFBA6CC55}.Release|Any CPU.ActiveCfg = Release|Any CPU
{30A0F689-AACC-48C8-8BFE-BC7BFBA6CC55}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5A20D5E9-BD27-4DD4-9F4F-97E0367DD2AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5A20D5E9-BD27-4DD4-9F4F-97E0367DD2AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5A20D5E9-BD27-4DD4-9F4F-97E0367DD2AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5A20D5E9-BD27-4DD4-9F4F-97E0367DD2AC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/LanMountainDesktop/LanMountainDesktop.csproj b/LanMountainDesktop/LanMountainDesktop.csproj
index 14098a4..71df87b 100644
--- a/LanMountainDesktop/LanMountainDesktop.csproj
+++ b/LanMountainDesktop/LanMountainDesktop.csproj
@@ -26,6 +26,8 @@
+
@@ -56,4 +58,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Services/PluginsInstallHelperClient.cs b/LanMountainDesktop/Services/PluginsInstallHelperClient.cs
new file mode 100644
index 0000000..5b01db8
--- /dev/null
+++ b/LanMountainDesktop/Services/PluginsInstallHelperClient.cs
@@ -0,0 +1,186 @@
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LanMountainDesktop.Services;
+
+internal sealed class PluginsInstallHelperClient
+{
+ private const int UserCanceledUacErrorCode = 1223;
+ private const string HelperExecutableName = "LanMountainDesktop.PluginsInstallHelper.exe";
+
+ public async Task InstallPackageAsync(
+ string packagePath,
+ string pluginsDirectory,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(packagePath);
+ ArgumentException.ThrowIfNullOrWhiteSpace(pluginsDirectory);
+
+ if (!OperatingSystem.IsWindows())
+ {
+ return new PluginsInstallHelperResult(
+ false,
+ null,
+ "Elevated helper install is only supported on Windows.");
+ }
+
+ var helperPath = ResolveHelperPath();
+ if (!File.Exists(helperPath))
+ {
+ return new PluginsInstallHelperResult(
+ false,
+ null,
+ $"Plugins install helper was not found at '{helperPath}'.");
+ }
+
+ var resultPath = Path.Combine(
+ Path.GetTempPath(),
+ "LanMountainDesktop",
+ "PluginInstallResults",
+ $"{Guid.NewGuid():N}.json");
+
+ Directory.CreateDirectory(Path.GetDirectoryName(resultPath)!);
+
+ try
+ {
+ using var process = StartHelperProcess(helperPath, packagePath, pluginsDirectory, resultPath);
+ if (process is null)
+ {
+ return new PluginsInstallHelperResult(false, null, "Failed to start plugins install helper.");
+ }
+
+ await process.WaitForExitAsync(cancellationToken);
+ var result = await ReadResultAsync(resultPath, cancellationToken);
+ if (result is not null)
+ {
+ return new PluginsInstallHelperResult(result.Success, result.InstalledPackagePath, result.ErrorMessage);
+ }
+
+ if (process.ExitCode == 0)
+ {
+ return new PluginsInstallHelperResult(
+ false,
+ null,
+ "Plugins install helper exited without producing a result file.");
+ }
+
+ return new PluginsInstallHelperResult(
+ false,
+ null,
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "Plugins install helper exited with code {0}.",
+ process.ExitCode));
+ }
+ catch (Win32Exception ex) when (ex.NativeErrorCode == UserCanceledUacErrorCode)
+ {
+ return new PluginsInstallHelperResult(false, null, "Administrator permission request was canceled.");
+ }
+ finally
+ {
+ TryDeleteFile(resultPath);
+ }
+ }
+
+ private static Process? StartHelperProcess(
+ string helperPath,
+ string packagePath,
+ string pluginsDirectory,
+ string resultPath)
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = helperPath,
+ Verb = "runas",
+ UseShellExecute = true,
+ WorkingDirectory = Path.GetDirectoryName(helperPath) ?? AppContext.BaseDirectory,
+ Arguments = string.Create(
+ CultureInfo.InvariantCulture,
+ $"--source {QuoteArgument(Path.GetFullPath(packagePath))} --plugins-dir {QuoteArgument(Path.GetFullPath(pluginsDirectory))} --result {QuoteArgument(Path.GetFullPath(resultPath))}")
+ };
+
+ return Process.Start(startInfo);
+ }
+
+ private static async Task ReadResultAsync(string resultPath, CancellationToken cancellationToken)
+ {
+ if (!File.Exists(resultPath))
+ {
+ return null;
+ }
+
+ await using var stream = File.OpenRead(resultPath);
+ return await JsonSerializer.DeserializeAsync(stream, cancellationToken: cancellationToken);
+ }
+
+ private static string ResolveHelperPath()
+ {
+ return Path.Combine(AppContext.BaseDirectory, "PluginsInstallHelper", HelperExecutableName);
+ }
+
+ private static string QuoteArgument(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ return "\"\"";
+ }
+
+ if (!value.Contains('"') && !value.Contains(' ') && !value.Contains('\t'))
+ {
+ return value;
+ }
+
+ var builder = new StringBuilder();
+ builder.Append('"');
+ foreach (var ch in value)
+ {
+ if (ch == '"')
+ {
+ builder.Append("\\\"");
+ }
+ else
+ {
+ builder.Append(ch);
+ }
+ }
+
+ builder.Append('"');
+ return builder.ToString();
+ }
+
+ private static void TryDeleteFile(string path)
+ {
+ try
+ {
+ if (File.Exists(path))
+ {
+ File.Delete(path);
+ }
+ }
+ catch
+ {
+ // Ignore temp file cleanup failures.
+ }
+ }
+
+ private sealed class HelperResultFile
+ {
+ public bool Success { get; init; }
+
+ public string? InstalledPackagePath { get; init; }
+
+ public string? ErrorMessage { get; init; }
+ }
+}
+
+internal sealed record PluginsInstallHelperResult(
+ bool Success,
+ string? InstalledPackagePath,
+ string? ErrorMessage);
diff --git a/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs b/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs
index 4136337..671cbec 100644
--- a/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs
+++ b/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs
@@ -1195,11 +1195,22 @@ public partial class MainWindow
var restoreButton = new Button
{
- Content = L("settings.launcher.restore_button", "Unhide"),
- MinWidth = 110,
- Padding = new Thickness(12, 6),
+ Width = 36,
+ Height = 36,
+ Padding = new Thickness(0),
+ Background = Brushes.Transparent,
+ BorderThickness = new Thickness(0),
Tag = new LauncherHiddenItemToken(hiddenItem.Kind, hiddenItem.Key)
};
+ restoreButton.Content = new FluentIcons.Avalonia.Fluent.SymbolIcon
+ {
+ Symbol = FluentIcons.Common.Symbol.Eye,
+ IconVariant = FluentIcons.Common.IconVariant.Regular,
+ FontSize = 18,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ };
+ ToolTip.SetTip(restoreButton, L("settings.launcher.restore_button", "Unhide"));
restoreButton.Click += OnRestoreLauncherHiddenItemClick;
return new SettingsExpanderItem
diff --git a/LanMountainDesktop/Views/MainWindow.Settings.cs b/LanMountainDesktop/Views/MainWindow.Settings.cs
index 14e3eea..3193565 100644
--- a/LanMountainDesktop/Views/MainWindow.Settings.cs
+++ b/LanMountainDesktop/Views/MainWindow.Settings.cs
@@ -158,6 +158,7 @@ public partial class MainWindow
}
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
+ UpdateVideoWallpaperPreviewVisibility();
}
private void OnNightModeChecked(object? sender, RoutedEventArgs e)
@@ -344,6 +345,7 @@ public partial class MainWindow
var placement = GetSelectedWallpaperPlacement();
DesktopWallpaperLayer.Background = CreateWallpaperBrush(_wallpaperBitmap, placement, false);
WallpaperPreviewViewport.Background = CreateWallpaperBrush(_wallpaperBitmap, placement, true);
+ UpdateVideoWallpaperPreviewVisibility();
}
private void UpdateWallpaperDisplay()
@@ -628,12 +630,99 @@ public partial class MainWindow
EnableHardwareDecoding = false
};
}
+ }
- if (_previewVideoWallpaperPlayer is null && WallpaperPreviewVideoView is not null)
+ private void EnsureDesktopVideoFrameRefreshTimer()
+ {
+ if (_desktopVideoFrameRefreshTimer is not null)
{
- _previewVideoWallpaperPlayer = new MediaPlayer(_libVlc);
- WallpaperPreviewVideoView.MediaPlayer = _previewVideoWallpaperPlayer;
+ return;
}
+
+ _desktopVideoFrameRefreshTimer = new DispatcherTimer
+ {
+ Interval = TimeSpan.FromMilliseconds(33)
+ };
+ _desktopVideoFrameRefreshTimer.Tick += OnDesktopVideoFrameRefreshTimerTick;
+ }
+
+ private void StartDesktopVideoFrameRefreshTimer()
+ {
+ EnsureDesktopVideoFrameRefreshTimer();
+ if (_desktopVideoFrameRefreshTimer?.IsEnabled == false)
+ {
+ _desktopVideoFrameRefreshTimer.Start();
+ }
+ }
+
+ private void StopDesktopVideoFrameRefreshTimer()
+ {
+ if (_desktopVideoFrameRefreshTimer?.IsEnabled == true)
+ {
+ _desktopVideoFrameRefreshTimer.Stop();
+ }
+ }
+
+ private void OnDesktopVideoFrameRefreshTimerTick(object? sender, EventArgs e)
+ {
+ PushDesktopVideoFrameToWallpaperImage();
+ }
+
+ private void UpdateVideoWallpaperPreviewVisibility()
+ {
+ var shouldShowPreview =
+ _wallpaperMediaType == WallpaperMediaType.Video &&
+ _isSettingsOpen &&
+ SettingsPage.IsVisible &&
+ WallpaperSettingsPanel.IsVisible &&
+ _wallpaperPreviewSnapshotBitmap is not null;
+
+ WallpaperPreviewVideoImage.IsVisible = shouldShowPreview;
+ if (shouldShowPreview && !ReferenceEquals(WallpaperPreviewVideoImage.Source, _wallpaperPreviewSnapshotBitmap))
+ {
+ WallpaperPreviewVideoImage.Source = _wallpaperPreviewSnapshotBitmap;
+ }
+ }
+
+ private void InvalidateVideoWallpaperPreviewSnapshot()
+ {
+ _wallpaperPreviewSnapshotPending = true;
+ _wallpaperPreviewSnapshotBitmap?.Dispose();
+ _wallpaperPreviewSnapshotBitmap = null;
+ WallpaperPreviewVideoImage.Source = null;
+ }
+
+ private void CaptureVideoWallpaperPreviewSnapshotFromStagingBuffer()
+ {
+ if (!_wallpaperPreviewSnapshotPending ||
+ _desktopVideoStagingBuffer is null ||
+ _desktopVideoFrameWidth <= 0 ||
+ _desktopVideoFrameHeight <= 0 ||
+ _desktopVideoFramePitch <= 0)
+ {
+ return;
+ }
+
+ _wallpaperPreviewSnapshotBitmap?.Dispose();
+ _wallpaperPreviewSnapshotBitmap = new WriteableBitmap(
+ new PixelSize(_desktopVideoFrameWidth, _desktopVideoFrameHeight),
+ new Vector(96, 96),
+ PixelFormat.Bgra8888,
+ AlphaFormat.Opaque);
+
+ using var framebuffer = _wallpaperPreviewSnapshotBitmap.Lock();
+ var rows = Math.Min(framebuffer.Size.Height, _desktopVideoFrameHeight);
+ var bytesPerRow = Math.Min(framebuffer.RowBytes, _desktopVideoFramePitch);
+ for (var row = 0; row < rows; row++)
+ {
+ var sourceOffset = row * _desktopVideoFramePitch;
+ var destinationPtr = IntPtr.Add(framebuffer.Address, row * framebuffer.RowBytes);
+ Marshal.Copy(_desktopVideoStagingBuffer, sourceOffset, destinationPtr, bytesPerRow);
+ }
+
+ _wallpaperPreviewSnapshotPending = false;
+ WallpaperPreviewVideoImage.Source = _wallpaperPreviewSnapshotBitmap;
+ UpdateVideoWallpaperPreviewVisibility();
}
private bool ConfigureDesktopVideoRenderer()
@@ -685,6 +774,7 @@ public partial class MainWindow
(uint)_desktopVideoFrameHeight,
(uint)_desktopVideoFramePitch);
DesktopVideoWallpaperImage.Source = _desktopVideoBitmap;
+ InvalidateVideoWallpaperPreviewSnapshot();
return true;
}
catch
@@ -745,31 +835,6 @@ public partial class MainWindow
private void OnDesktopVideoFrameDisplay(IntPtr opaque, IntPtr picture)
{
Interlocked.Exchange(ref _desktopVideoFrameDirtyFlag, 1);
- ScheduleDesktopVideoFrameUiRefresh();
- }
-
- private void ScheduleDesktopVideoFrameUiRefresh()
- {
- if (Interlocked.Exchange(ref _desktopVideoFrameUiRefreshScheduledFlag, 1) == 1)
- {
- return;
- }
-
- Dispatcher.UIThread.Post(() =>
- {
- try
- {
- PushDesktopVideoFrameToWallpaperImage();
- }
- finally
- {
- Interlocked.Exchange(ref _desktopVideoFrameUiRefreshScheduledFlag, 0);
- if (Volatile.Read(ref _desktopVideoFrameDirtyFlag) == 1)
- {
- ScheduleDesktopVideoFrameUiRefresh();
- }
- }
- }, DispatcherPriority.Render);
}
private void PushDesktopVideoFrameToWallpaperImage()
@@ -812,18 +877,22 @@ public partial class MainWindow
{
DesktopVideoWallpaperImage.Source = _desktopVideoBitmap;
}
+
+ CaptureVideoWallpaperPreviewSnapshotFromStagingBuffer();
}
private void ReleaseDesktopVideoRendererResources()
{
Interlocked.Exchange(ref _desktopVideoFrameDirtyFlag, 0);
- Interlocked.Exchange(ref _desktopVideoFrameUiRefreshScheduledFlag, 0);
if (DesktopVideoWallpaperImage is not null)
{
DesktopVideoWallpaperImage.Source = null;
}
+ InvalidateVideoWallpaperPreviewSnapshot();
+ WallpaperPreviewVideoImage.Source = null;
+
_desktopVideoBitmap?.Dispose();
_desktopVideoBitmap = null;
_desktopVideoStagingBuffer = null;
@@ -855,10 +924,8 @@ public partial class MainWindow
{
EnsureVideoWallpaperPlayers();
if (_videoWallpaperPlayer is null ||
- _previewVideoWallpaperPlayer is null ||
_libVlc is null ||
- DesktopVideoWallpaperImage is null ||
- WallpaperPreviewVideoView is null)
+ DesktopVideoWallpaperImage is null)
{
_wallpaperStatus = L("settings.wallpaper.video_player_unavailable", "Video player is unavailable.");
StopVideoWallpaper();
@@ -873,15 +940,13 @@ public partial class MainWindow
}
_videoWallpaperMedia?.Dispose();
- _previewVideoWallpaperMedia?.Dispose();
_videoWallpaperMedia = new Media(_libVlc, new Uri(videoPath));
- _previewVideoWallpaperMedia = new Media(_libVlc, new Uri(videoPath));
_videoWallpaperMedia.AddOption(":input-repeat=65535");
- _previewVideoWallpaperMedia.AddOption(":input-repeat=65535");
+ InvalidateVideoWallpaperPreviewSnapshot();
_videoWallpaperPlayer.Play(_videoWallpaperMedia);
- _previewVideoWallpaperPlayer.Play(_previewVideoWallpaperMedia);
+ StartDesktopVideoFrameRefreshTimer();
DesktopVideoWallpaperImage.IsVisible = true;
- WallpaperPreviewVideoView.IsVisible = true;
+ UpdateVideoWallpaperPreviewVisibility();
}
catch (Exception ex)
{
@@ -897,26 +962,17 @@ public partial class MainWindow
DesktopVideoWallpaperImage.IsVisible = false;
}
- if (WallpaperPreviewVideoView is not null)
- {
- WallpaperPreviewVideoView.IsVisible = false;
- }
+ WallpaperPreviewVideoImage.IsVisible = false;
if (_videoWallpaperPlayer is not null)
{
_videoWallpaperPlayer.Stop();
}
- if (_previewVideoWallpaperPlayer is not null)
- {
- _previewVideoWallpaperPlayer.Stop();
- }
-
+ StopDesktopVideoFrameRefreshTimer();
ReleaseDesktopVideoRendererResources();
_videoWallpaperMedia?.Dispose();
_videoWallpaperMedia = null;
- _previewVideoWallpaperMedia?.Dispose();
- _previewVideoWallpaperMedia = null;
}
private void PersistSettings()
@@ -2379,7 +2435,14 @@ public partial class MainWindow
_isSettingsOpen = true;
UpdateDesktopPageAwareComponentContext();
UpdateAdaptiveTextSystem();
- ApplyWallpaperBrush();
+ if (_wallpaperMediaType == WallpaperMediaType.Video)
+ {
+ UpdateVideoWallpaperPreviewVisibility();
+ }
+ else
+ {
+ ApplyWallpaperBrush();
+ }
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
if (_settingsContentPanelTransform is not null)
{
@@ -2387,6 +2450,7 @@ public partial class MainWindow
}
SettingsPage.IsVisible = true;
SettingsPage.Opacity = 0;
+ UpdateVideoWallpaperPreviewVisibility();
UpdateSettingsViewportInsets(Math.Max(1, _currentDesktopCellSize));
UpdateWallpaperPreviewLayout();
@@ -2416,7 +2480,11 @@ public partial class MainWindow
_isSettingsOpen = false;
UpdateDesktopPageAwareComponentContext();
UpdateAdaptiveTextSystem();
- ApplyWallpaperBrush();
+ UpdateVideoWallpaperPreviewVisibility();
+ if (_wallpaperMediaType != WallpaperMediaType.Video)
+ {
+ ApplyWallpaperBrush();
+ }
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
if (immediate)
@@ -2608,7 +2676,7 @@ public partial class MainWindow
internal Border WallpaperPreviewHost => WallpaperSettingsPanel.FindControl("WallpaperPreviewHost")!;
internal Border WallpaperPreviewFrame => WallpaperSettingsPanel.FindControl("WallpaperPreviewFrame")!;
internal Border WallpaperPreviewViewport => WallpaperSettingsPanel.FindControl("WallpaperPreviewViewport")!;
- internal LibVLCSharp.Avalonia.VideoView? WallpaperPreviewVideoView => WallpaperSettingsPanel.FindControl("WallpaperPreviewVideoView");
+ internal Image WallpaperPreviewVideoImage => WallpaperSettingsPanel.FindControl("WallpaperPreviewVideoImage")!;
internal Grid WallpaperPreviewGrid => WallpaperSettingsPanel.FindControl("WallpaperPreviewGrid")!;
internal Border WallpaperPreviewTopStatusBarHost => WallpaperSettingsPanel.FindControl("WallpaperPreviewTopStatusBarHost")!;
internal StackPanel WallpaperPreviewTopStatusComponentsPanel => WallpaperSettingsPanel.FindControl("WallpaperPreviewTopStatusComponentsPanel")!;
diff --git a/LanMountainDesktop/Views/MainWindow.axaml.cs b/LanMountainDesktop/Views/MainWindow.axaml.cs
index 2c3742d..aec08bc 100644
--- a/LanMountainDesktop/Views/MainWindow.axaml.cs
+++ b/LanMountainDesktop/Views/MainWindow.axaml.cs
@@ -124,21 +124,21 @@ public partial class MainWindow : Window
private LibVLC? _libVlc;
private MediaPlayer? _videoWallpaperPlayer;
private Media? _videoWallpaperMedia;
- private MediaPlayer? _previewVideoWallpaperPlayer;
- private Media? _previewVideoWallpaperMedia;
private readonly object _desktopVideoFrameSync = new();
private MediaPlayer.LibVLCVideoLockCb? _desktopVideoLockCallback;
private MediaPlayer.LibVLCVideoUnlockCb? _desktopVideoUnlockCallback;
private MediaPlayer.LibVLCVideoDisplayCb? _desktopVideoDisplayCallback;
+ private DispatcherTimer? _desktopVideoFrameRefreshTimer;
private IntPtr _desktopVideoFrameBufferPtr;
private byte[]? _desktopVideoStagingBuffer;
private WriteableBitmap? _desktopVideoBitmap;
+ private WriteableBitmap? _wallpaperPreviewSnapshotBitmap;
private int _desktopVideoFrameWidth;
private int _desktopVideoFrameHeight;
private int _desktopVideoFramePitch;
private int _desktopVideoFrameBufferSize;
private int _desktopVideoFrameDirtyFlag;
- private int _desktopVideoFrameUiRefreshScheduledFlag;
+ private bool _wallpaperPreviewSnapshotPending;
private string? _wallpaperPath;
private string _wallpaperStatus = "Current background uses solid color.";
private IReadOnlyList _recommendedColors = Array.Empty();
@@ -384,15 +384,15 @@ public partial class MainWindow : Window
{
PersistSettings();
StopVideoWallpaper();
- _previewVideoWallpaperMedia?.Dispose();
- _previewVideoWallpaperMedia = null;
- _previewVideoWallpaperPlayer?.Dispose();
- _previewVideoWallpaperPlayer = null;
DisposeLauncherResources();
_videoWallpaperMedia?.Dispose();
_videoWallpaperMedia = null;
_videoWallpaperPlayer?.Dispose();
_videoWallpaperPlayer = null;
+ _desktopVideoFrameRefreshTimer?.Stop();
+ _desktopVideoFrameRefreshTimer = null;
+ _wallpaperPreviewSnapshotBitmap?.Dispose();
+ _wallpaperPreviewSnapshotBitmap = null;
_libVlc?.Dispose();
_libVlc = null;
if (_weatherDataService is IDisposable weatherServiceDisposable)
diff --git a/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml
index 03039e6..1d24376 100644
--- a/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml
@@ -6,7 +6,6 @@
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:ic="using:FluentIcons.Avalonia.Fluent"
xmlns:comp="using:LanMountainDesktop.Views.Components"
- xmlns:vlc="clr-namespace:LibVLCSharp.Avalonia;assembly=LibVLCSharp.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="LanMountainDesktop.Views.SettingsPages.WallpaperSettingsPage">
-
+
WallpaperSettingsPanel.FindControl("WallpaperPreviewHost")!;
internal Border WallpaperPreviewFrame => WallpaperSettingsPanel.FindControl("WallpaperPreviewFrame")!;
internal Border WallpaperPreviewViewport => WallpaperSettingsPanel.FindControl("WallpaperPreviewViewport")!;
- internal LibVLCSharp.Avalonia.VideoView? WallpaperPreviewVideoView => WallpaperSettingsPanel.FindControl("WallpaperPreviewVideoView");
+ internal Image WallpaperPreviewVideoImage => WallpaperSettingsPanel.FindControl("WallpaperPreviewVideoImage")!;
internal Grid WallpaperPreviewGrid => WallpaperSettingsPanel.FindControl("WallpaperPreviewGrid")!;
internal Border WallpaperPreviewTopStatusBarHost => WallpaperSettingsPanel.FindControl("WallpaperPreviewTopStatusBarHost")!;
internal StackPanel WallpaperPreviewTopStatusComponentsPanel => WallpaperSettingsPanel.FindControl("WallpaperPreviewTopStatusComponentsPanel")!;
diff --git a/LanMountainDesktop/Views/SettingsWindow.Core.cs b/LanMountainDesktop/Views/SettingsWindow.Core.cs
index 55bbcf7..7c03b04 100644
--- a/LanMountainDesktop/Views/SettingsWindow.Core.cs
+++ b/LanMountainDesktop/Views/SettingsWindow.Core.cs
@@ -29,6 +29,8 @@ public partial class SettingsWindow
_previewVideoWallpaperPlayer = null;
_previewVideoWallpaperMedia?.Dispose();
_previewVideoWallpaperMedia = null;
+ _previewVideoFrameRefreshTimer?.Stop();
+ _previewVideoFrameRefreshTimer = null;
_libVlc?.Dispose();
_libVlc = null;
@@ -254,6 +256,7 @@ public partial class SettingsWindow
}
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
+ SyncVideoWallpaperPreviewPlayback();
}
private void PersistSettings()
diff --git a/LanMountainDesktop/Views/SettingsWindow.WallpaperTheme.cs b/LanMountainDesktop/Views/SettingsWindow.WallpaperTheme.cs
index 8895e35..aaef17d 100644
--- a/LanMountainDesktop/Views/SettingsWindow.WallpaperTheme.cs
+++ b/LanMountainDesktop/Views/SettingsWindow.WallpaperTheme.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
+using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
@@ -12,6 +13,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Platform.Storage;
+using Avalonia.Threading;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
using LibVLCSharp.Shared;
@@ -157,7 +159,7 @@ public partial class SettingsWindow
{
DesktopWallpaperLayer.Background = Brushes.Transparent;
WallpaperPreviewViewport.Background = GetThemeDefaultDesktopBackground();
- PlayVideoWallpaper(_wallpaperVideoPath);
+ SyncVideoWallpaperPreviewPlayback();
return;
}
@@ -175,6 +177,36 @@ public partial class SettingsWindow
WallpaperPreviewViewport.Background = CreateWallpaperBrush(_wallpaperBitmap, placement, true);
}
+ private void SyncVideoWallpaperPreviewPlayback()
+ {
+ var shouldPlay =
+ _wallpaperMediaType == WallpaperMediaType.Video &&
+ !string.IsNullOrWhiteSpace(_wallpaperVideoPath) &&
+ WallpaperSettingsPanel.IsVisible;
+
+ if (!shouldPlay)
+ {
+ if (_previewVideoWallpaperPlayer?.IsPlaying == true)
+ {
+ StopPreviewVideoCapture(clearSnapshot: false);
+ }
+
+ WallpaperPreviewVideoImage.IsVisible = WallpaperPreviewVideoImage.Source is not null && WallpaperSettingsPanel.IsVisible;
+ return;
+ }
+
+ if (WallpaperPreviewVideoImage.Source is not null)
+ {
+ WallpaperPreviewVideoImage.IsVisible = true;
+ return;
+ }
+
+ if (_previewVideoWallpaperMedia is null || _previewVideoSnapshotPending)
+ {
+ PlayVideoWallpaper(_wallpaperVideoPath!);
+ }
+ }
+
private void UpdateWallpaperDisplay()
{
WallpaperPathTextBlock.Text = string.IsNullOrWhiteSpace(_wallpaperPath)
@@ -417,11 +449,239 @@ public partial class SettingsWindow
private void EnsureVideoWallpaperPlayers()
{
Core.Initialize();
- _libVlc ??= new LibVLC();
- _previewVideoWallpaperPlayer ??= new MediaPlayer(_libVlc);
- if (WallpaperPreviewVideoView is not null)
+ _libVlc ??= new LibVLC("--quiet");
+ if (_previewVideoWallpaperPlayer is null)
{
- WallpaperPreviewVideoView.MediaPlayer = _previewVideoWallpaperPlayer;
+ _previewVideoWallpaperPlayer = new MediaPlayer(_libVlc)
+ {
+ EnableHardwareDecoding = false
+ };
+ }
+ }
+
+ private void EnsurePreviewVideoFrameRefreshTimer()
+ {
+ if (_previewVideoFrameRefreshTimer is not null)
+ {
+ return;
+ }
+
+ _previewVideoFrameRefreshTimer = new DispatcherTimer
+ {
+ Interval = TimeSpan.FromMilliseconds(33)
+ };
+ _previewVideoFrameRefreshTimer.Tick += OnPreviewVideoFrameRefreshTimerTick;
+ }
+
+ private void StartPreviewVideoFrameRefreshTimer()
+ {
+ EnsurePreviewVideoFrameRefreshTimer();
+ if (_previewVideoFrameRefreshTimer?.IsEnabled == false)
+ {
+ _previewVideoFrameRefreshTimer.Start();
+ }
+ }
+
+ private void StopPreviewVideoFrameRefreshTimer()
+ {
+ if (_previewVideoFrameRefreshTimer?.IsEnabled == true)
+ {
+ _previewVideoFrameRefreshTimer.Stop();
+ }
+ }
+
+ private void OnPreviewVideoFrameRefreshTimerTick(object? sender, EventArgs e)
+ {
+ PushPreviewVideoFrameToWallpaperImage();
+ }
+
+ private void StopPreviewVideoCapture(bool clearSnapshot)
+ {
+ WallpaperPreviewVideoImage.IsVisible = false;
+ _previewVideoWallpaperPlayer?.Stop();
+ StopPreviewVideoFrameRefreshTimer();
+ _previewVideoWallpaperMedia?.Dispose();
+ _previewVideoWallpaperMedia = null;
+ _previewVideoSnapshotPending = false;
+
+ if (clearSnapshot)
+ {
+ ReleasePreviewVideoRendererResources();
+ }
+ }
+
+ private bool ConfigurePreviewVideoRenderer()
+ {
+ if (_previewVideoWallpaperPlayer is null)
+ {
+ return false;
+ }
+
+ var hostWidth = Math.Max(1, WallpaperPreviewViewport.Bounds.Width);
+ var hostHeight = Math.Max(1, WallpaperPreviewViewport.Bounds.Height);
+ var pixelWidth = Math.Max(1, (int)Math.Round(hostWidth * RenderScaling));
+ var pixelHeight = Math.Max(1, (int)Math.Round(hostHeight * RenderScaling));
+ const int maxPixelCount = 1280 * 720;
+ var pixelCount = (long)pixelWidth * pixelHeight;
+ if (pixelCount > maxPixelCount)
+ {
+ var scale = Math.Sqrt((double)maxPixelCount / pixelCount);
+ pixelWidth = Math.Max(1, (int)Math.Round(pixelWidth * scale));
+ pixelHeight = Math.Max(1, (int)Math.Round(pixelHeight * scale));
+ }
+
+ var pitch = pixelWidth * 4;
+ var bufferSize = pitch * pixelHeight;
+ if (bufferSize <= 0)
+ {
+ return false;
+ }
+
+ if (pixelWidth == _previewVideoFrameWidth &&
+ pixelHeight == _previewVideoFrameHeight &&
+ _previewVideoFrameBufferPtr != IntPtr.Zero &&
+ _previewVideoBitmap is not null)
+ {
+ return true;
+ }
+
+ ReleasePreviewVideoRendererResources();
+
+ try
+ {
+ _previewVideoFrameWidth = pixelWidth;
+ _previewVideoFrameHeight = pixelHeight;
+ _previewVideoFramePitch = pitch;
+ _previewVideoFrameBufferSize = bufferSize;
+ _previewVideoFrameBufferPtr = Marshal.AllocHGlobal(_previewVideoFrameBufferSize);
+ _previewVideoStagingBuffer = new byte[_previewVideoFrameBufferSize];
+ _previewVideoBitmap = new WriteableBitmap(
+ new PixelSize(_previewVideoFrameWidth, _previewVideoFrameHeight),
+ new Vector(96, 96),
+ PixelFormat.Bgra8888,
+ AlphaFormat.Opaque);
+ EnsurePreviewVideoCallbacks();
+ _previewVideoWallpaperPlayer.SetVideoCallbacks(
+ _previewVideoLockCallback!,
+ _previewVideoUnlockCallback!,
+ _previewVideoDisplayCallback!);
+ _previewVideoWallpaperPlayer.SetVideoFormat(
+ "RV32",
+ (uint)_previewVideoFrameWidth,
+ (uint)_previewVideoFrameHeight,
+ (uint)_previewVideoFramePitch);
+ WallpaperPreviewVideoImage.Source = _previewVideoBitmap;
+ return true;
+ }
+ catch
+ {
+ ReleasePreviewVideoRendererResources();
+ return false;
+ }
+ }
+
+ private void EnsurePreviewVideoCallbacks()
+ {
+ _previewVideoLockCallback ??= OnPreviewVideoFrameLock;
+ _previewVideoUnlockCallback ??= OnPreviewVideoFrameUnlock;
+ _previewVideoDisplayCallback ??= OnPreviewVideoFrameDisplay;
+ }
+
+ private IntPtr OnPreviewVideoFrameLock(IntPtr opaque, IntPtr planes)
+ {
+ Monitor.Enter(_previewVideoFrameSync);
+ if (_previewVideoFrameBufferPtr == IntPtr.Zero)
+ {
+ Marshal.WriteIntPtr(planes, IntPtr.Zero);
+ Monitor.Exit(_previewVideoFrameSync);
+ return IntPtr.Zero;
+ }
+
+ Marshal.WriteIntPtr(planes, _previewVideoFrameBufferPtr);
+ return IntPtr.Zero;
+ }
+
+ private void OnPreviewVideoFrameUnlock(IntPtr opaque, IntPtr picture, IntPtr planes)
+ {
+ if (Monitor.IsEntered(_previewVideoFrameSync))
+ {
+ Monitor.Exit(_previewVideoFrameSync);
+ }
+ }
+
+ private void OnPreviewVideoFrameDisplay(IntPtr opaque, IntPtr picture)
+ {
+ Interlocked.Exchange(ref _previewVideoFrameDirtyFlag, 1);
+ }
+
+ private void PushPreviewVideoFrameToWallpaperImage()
+ {
+ if (Interlocked.Exchange(ref _previewVideoFrameDirtyFlag, 0) == 0)
+ {
+ return;
+ }
+
+ if (_previewVideoBitmap is null ||
+ _previewVideoStagingBuffer is null ||
+ _previewVideoFrameBufferPtr == IntPtr.Zero ||
+ _previewVideoFrameBufferSize <= 0)
+ {
+ return;
+ }
+
+ lock (_previewVideoFrameSync)
+ {
+ if (_previewVideoFrameBufferPtr == IntPtr.Zero)
+ {
+ return;
+ }
+
+ Marshal.Copy(_previewVideoFrameBufferPtr, _previewVideoStagingBuffer, 0, _previewVideoFrameBufferSize);
+ }
+
+ using var framebuffer = _previewVideoBitmap.Lock();
+ var rows = Math.Min(framebuffer.Size.Height, _previewVideoFrameHeight);
+ var bytesPerRow = Math.Min(framebuffer.RowBytes, _previewVideoFramePitch);
+ for (var row = 0; row < rows; row++)
+ {
+ var sourceOffset = row * _previewVideoFramePitch;
+ var destinationPtr = IntPtr.Add(framebuffer.Address, row * framebuffer.RowBytes);
+ Marshal.Copy(_previewVideoStagingBuffer, sourceOffset, destinationPtr, bytesPerRow);
+ }
+
+ if (!ReferenceEquals(WallpaperPreviewVideoImage.Source, _previewVideoBitmap))
+ {
+ WallpaperPreviewVideoImage.Source = _previewVideoBitmap;
+ }
+
+ if (_previewVideoSnapshotPending)
+ {
+ _previewVideoSnapshotPending = false;
+ WallpaperPreviewVideoImage.IsVisible = WallpaperSettingsPanel.IsVisible;
+ StopPreviewVideoCapture(clearSnapshot: false);
+ WallpaperPreviewVideoImage.IsVisible = WallpaperSettingsPanel.IsVisible;
+ }
+ }
+
+ private void ReleasePreviewVideoRendererResources()
+ {
+ Interlocked.Exchange(ref _previewVideoFrameDirtyFlag, 0);
+ WallpaperPreviewVideoImage.Source = null;
+ _previewVideoBitmap?.Dispose();
+ _previewVideoBitmap = null;
+ _previewVideoStagingBuffer = null;
+ _previewVideoFrameWidth = 0;
+ _previewVideoFrameHeight = 0;
+ _previewVideoFramePitch = 0;
+ _previewVideoFrameBufferSize = 0;
+
+ lock (_previewVideoFrameSync)
+ {
+ if (_previewVideoFrameBufferPtr != IntPtr.Zero)
+ {
+ Marshal.FreeHGlobal(_previewVideoFrameBufferPtr);
+ _previewVideoFrameBufferPtr = IntPtr.Zero;
+ }
}
}
@@ -437,7 +697,14 @@ public partial class SettingsWindow
try
{
EnsureVideoWallpaperPlayers();
- if (_previewVideoWallpaperPlayer is null || _libVlc is null || WallpaperPreviewVideoView is null)
+ if (_previewVideoWallpaperPlayer is null || _libVlc is null)
+ {
+ _wallpaperStatus = L("settings.wallpaper.video_player_unavailable", "Video player is unavailable.");
+ StopVideoWallpaper();
+ return;
+ }
+
+ if (!ConfigurePreviewVideoRenderer())
{
_wallpaperStatus = L("settings.wallpaper.video_player_unavailable", "Video player is unavailable.");
StopVideoWallpaper();
@@ -447,8 +714,10 @@ public partial class SettingsWindow
_previewVideoWallpaperMedia?.Dispose();
_previewVideoWallpaperMedia = new Media(_libVlc, new Uri(videoPath));
_previewVideoWallpaperMedia.AddOption(":input-repeat=65535");
+ _previewVideoSnapshotPending = true;
+ WallpaperPreviewVideoImage.IsVisible = false;
_previewVideoWallpaperPlayer.Play(_previewVideoWallpaperMedia);
- WallpaperPreviewVideoView.IsVisible = true;
+ StartPreviewVideoFrameRefreshTimer();
}
catch (Exception ex)
{
@@ -459,14 +728,7 @@ public partial class SettingsWindow
private void StopVideoWallpaper()
{
- if (WallpaperPreviewVideoView is not null)
- {
- WallpaperPreviewVideoView.IsVisible = false;
- }
-
- _previewVideoWallpaperPlayer?.Stop();
- _previewVideoWallpaperMedia?.Dispose();
- _previewVideoWallpaperMedia = null;
+ StopPreviewVideoCapture(clearSnapshot: true);
}
private void OnRecommendedColorClick(object? sender, RoutedEventArgs e)
diff --git a/LanMountainDesktop/Views/SettingsWindow.WeatherLauncher.cs b/LanMountainDesktop/Views/SettingsWindow.WeatherLauncher.cs
index 35846fa..6bd2051 100644
--- a/LanMountainDesktop/Views/SettingsWindow.WeatherLauncher.cs
+++ b/LanMountainDesktop/Views/SettingsWindow.WeatherLauncher.cs
@@ -873,11 +873,22 @@ public partial class SettingsWindow
var restoreButton = new Button
{
- Content = L("settings.launcher.restore_button", "Unhide"),
- MinWidth = 110,
- Padding = new Thickness(12, 6),
+ Width = 36,
+ Height = 36,
+ Padding = new Thickness(0),
+ Background = Brushes.Transparent,
+ BorderThickness = new Thickness(0),
Tag = new LauncherHiddenItemToken(hiddenItem.Kind, hiddenItem.Key)
};
+ restoreButton.Content = new FluentIcons.Avalonia.Fluent.SymbolIcon
+ {
+ Symbol = FluentIcons.Common.Symbol.Eye,
+ IconVariant = FluentIcons.Common.IconVariant.Regular,
+ FontSize = 18,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ };
+ ToolTip.SetTip(restoreButton, L("settings.launcher.restore_button", "Unhide"));
restoreButton.Click += OnRestoreLauncherHiddenItemClick;
return new SettingsExpanderItem
diff --git a/LanMountainDesktop/Views/SettingsWindow.axaml.cs b/LanMountainDesktop/Views/SettingsWindow.axaml.cs
index 4ad5fb0..0ebdada 100644
--- a/LanMountainDesktop/Views/SettingsWindow.axaml.cs
+++ b/LanMountainDesktop/Views/SettingsWindow.axaml.cs
@@ -129,6 +129,20 @@ public partial class SettingsWindow : Window
private MediaPlayer? _previewVideoWallpaperPlayer;
private Media? _previewVideoWallpaperMedia;
private LibVLC? _libVlc;
+ private readonly object _previewVideoFrameSync = new();
+ private MediaPlayer.LibVLCVideoLockCb? _previewVideoLockCallback;
+ private MediaPlayer.LibVLCVideoUnlockCb? _previewVideoUnlockCallback;
+ private MediaPlayer.LibVLCVideoDisplayCb? _previewVideoDisplayCallback;
+ private DispatcherTimer? _previewVideoFrameRefreshTimer;
+ private IntPtr _previewVideoFrameBufferPtr;
+ private byte[]? _previewVideoStagingBuffer;
+ private WriteableBitmap? _previewVideoBitmap;
+ private int _previewVideoFrameWidth;
+ private int _previewVideoFrameHeight;
+ private int _previewVideoFramePitch;
+ private int _previewVideoFrameBufferSize;
+ private int _previewVideoFrameDirtyFlag;
+ private bool _previewVideoSnapshotPending;
private string? _wallpaperPath;
private string _wallpaperStatus = "Current background uses solid color.";
private IReadOnlyList _recommendedColors = Array.Empty();
diff --git a/LanMountainDesktop/installer/LanMountainDesktop.iss b/LanMountainDesktop/installer/LanMountainDesktop.iss
index ba690a3..2b1feb2 100644
--- a/LanMountainDesktop/installer/LanMountainDesktop.iss
+++ b/LanMountainDesktop/installer/LanMountainDesktop.iss
@@ -113,7 +113,7 @@ Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Registry]
-Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "{#MyAppName}"; ValueData: """{app}\{#MyAppExeName}"""; Tasks: startup; Flags: uninsdeletevalue
+Root: HKA; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "{#MyAppName}"; ValueData: """{app}\{#MyAppExeName}"""; Tasks: startup; Flags: uninsdeletevalue
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
diff --git a/LanMountainDesktop/plugins/PluginMarketInstallService.cs b/LanMountainDesktop/plugins/PluginMarketInstallService.cs
index 273fe51..796902f 100644
--- a/LanMountainDesktop/plugins/PluginMarketInstallService.cs
+++ b/LanMountainDesktop/plugins/PluginMarketInstallService.cs
@@ -13,6 +13,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
internal sealed class AirAppMarketInstallService : IDisposable
{
private readonly PluginRuntimeService _runtime;
+ private readonly PluginsInstallHelperClient _helperClient = new();
private readonly HttpClient _httpClient;
private readonly ResumableDownloadService _downloadService;
private readonly AirAppMarketReleaseResolverService _releaseResolverService;
@@ -89,7 +90,28 @@ internal sealed class AirAppMarketInstallService : IDisposable
$"SHA-256 mismatch. Expected {plugin.Sha256}, actual {actualHash}.");
}
- var manifest = _runtime.InstallPluginPackage(downloadPath);
+ PluginManifest manifest;
+ if (OperatingSystem.IsWindows())
+ {
+ var helperResult = await _helperClient.InstallPackageAsync(
+ downloadPath,
+ _runtime.PluginsDirectory,
+ cancellationToken);
+ if (!helperResult.Success || string.IsNullOrWhiteSpace(helperResult.InstalledPackagePath))
+ {
+ return new AirAppMarketInstallResult(
+ false,
+ null,
+ helperResult.ErrorMessage ?? "Plugins install helper failed.");
+ }
+
+ manifest = _runtime.RegisterInstalledPluginPackage(helperResult.InstalledPackagePath);
+ }
+ else
+ {
+ manifest = _runtime.InstallPluginPackage(downloadPath);
+ }
+
AppLogger.Info(
"PluginMarket",
$"Install staged successfully. PluginId='{manifest.Id}'; InstalledName='{manifest.Name}'; PackagePath='{downloadPath}'.");
diff --git a/LanMountainDesktop/plugins/PluginRuntimeService.cs b/LanMountainDesktop/plugins/PluginRuntimeService.cs
index 2e26c93..159429a 100644
--- a/LanMountainDesktop/plugins/PluginRuntimeService.cs
+++ b/LanMountainDesktop/plugins/PluginRuntimeService.cs
@@ -196,6 +196,14 @@ public sealed class PluginRuntimeService : IDisposable
}
}
+ public PluginManifest RegisterInstalledPluginPackage(string packagePath)
+ {
+ lock (_packageMutationGate)
+ {
+ return RegisterInstalledPluginPackageCore(packagePath);
+ }
+ }
+
public bool DeleteInstalledPlugin(string pluginId)
{
lock (_packageMutationGate)
@@ -288,6 +296,25 @@ public sealed class PluginRuntimeService : IDisposable
return new PluginPackageInstallResult(manifest, replacedExisting, RestartRequired: true);
}
+ private PluginManifest RegisterInstalledPluginPackageCore(string packagePath)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(packagePath);
+
+ var fullPackagePath = Path.GetFullPath(packagePath);
+ if (!File.Exists(fullPackagePath))
+ {
+ throw new FileNotFoundException($"Plugin package '{fullPackagePath}' was not found.", fullPackagePath);
+ }
+
+ var manifest = ReadManifestFromPackage(fullPackagePath);
+ AppLogger.Info(
+ "PluginRuntime",
+ $"Registering externally installed package. PluginId='{manifest.Id}'; Source='{fullPackagePath}'.");
+ UpdateCatalogAfterPackageInstall(manifest, fullPackagePath);
+ PendingRestartStateService.SetPending(PendingRestartStateService.PluginCatalogReason, true);
+ return manifest;
+ }
+
public void Dispose()
{
UnloadInstalledPlugins();