From 2781d7e0d9a4099ec640cb46554397081be90509 Mon Sep 17 00:00:00 2001 From: lincube Date: Wed, 11 Mar 2026 06:38:11 +0800 Subject: [PATCH] 0.5.13 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 插件市场安装优化 --- ...ountainDesktop.PluginsInstallHelper.csproj | 12 + .../Program.cs | 177 +++++++++++ LanMountainDesktop.sln | 6 + LanMountainDesktop/LanMountainDesktop.csproj | 20 ++ .../Services/PluginsInstallHelperClient.cs | 186 +++++++++++ .../Views/MainWindow.DesktopPaging.cs | 17 +- .../Views/MainWindow.Settings.cs | 170 +++++++--- LanMountainDesktop/Views/MainWindow.axaml.cs | 14 +- .../SettingsPages/WallpaperSettingsPage.axaml | 12 +- .../Views/SettingsWindow.Controls.cs | 3 +- .../Views/SettingsWindow.Core.cs | 3 + .../Views/SettingsWindow.WallpaperTheme.cs | 292 +++++++++++++++++- .../Views/SettingsWindow.WeatherLauncher.cs | 17 +- .../Views/SettingsWindow.axaml.cs | 14 + .../installer/LanMountainDesktop.iss | 2 +- .../plugins/PluginMarketInstallService.cs | 24 +- .../plugins/PluginRuntimeService.cs | 27 ++ 17 files changed, 907 insertions(+), 89 deletions(-) create mode 100644 LanMountainDesktop.PluginsInstallHelper/LanMountainDesktop.PluginsInstallHelper.csproj create mode 100644 LanMountainDesktop.PluginsInstallHelper/Program.cs create mode 100644 LanMountainDesktop/Services/PluginsInstallHelperClient.cs 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();