mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2781d7e0d9 |
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
177
LanMountainDesktop.PluginsInstallHelper/Program.cs
Normal file
177
LanMountainDesktop.PluginsInstallHelper/Program.cs
Normal file
@@ -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<int> 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 <path> --plugins-dir <path> --result <path>.");
|
||||
}
|
||||
|
||||
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<string, string> ParseArgs(string[] args)
|
||||
{
|
||||
var values = new Dictionary<string, string>(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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.PluginsInstallHelper\LanMountainDesktop.PluginsInstallHelper.csproj"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -56,4 +58,22 @@
|
||||
<PackageReference Include="WebView.Avalonia.Desktop" Version="11.0.0.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyPluginsInstallHelperToOutput" AfterTargets="Build">
|
||||
<ItemGroup>
|
||||
<PluginsInstallHelperFiles Include="..\LanMountainDesktop.PluginsInstallHelper\bin\$(Configuration)\net10.0\**\*.*" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(PluginsInstallHelperFiles)"
|
||||
DestinationFiles="@(PluginsInstallHelperFiles->'$(OutDir)PluginsInstallHelper\%(RecursiveDir)%(Filename)%(Extension)')"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
<Target Name="CopyPluginsInstallHelperToPublish" AfterTargets="Publish" Condition="'$(PublishDir)' != ''">
|
||||
<ItemGroup>
|
||||
<PluginsInstallHelperPublishFiles Include="..\LanMountainDesktop.PluginsInstallHelper\bin\$(Configuration)\net10.0\**\*.*" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(PluginsInstallHelperPublishFiles)"
|
||||
DestinationFiles="@(PluginsInstallHelperPublishFiles->'$(PublishDir)PluginsInstallHelper\%(RecursiveDir)%(Filename)%(Extension)')"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
186
LanMountainDesktop/Services/PluginsInstallHelperClient.cs
Normal file
186
LanMountainDesktop/Services/PluginsInstallHelperClient.cs
Normal file
@@ -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<PluginsInstallHelperResult> 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<HelperResultFile?> ReadResultAsync(string resultPath, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(resultPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
await using var stream = File.OpenRead(resultPath);
|
||||
return await JsonSerializer.DeserializeAsync<HelperResultFile>(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);
|
||||
@@ -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
|
||||
|
||||
@@ -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<Border>("WallpaperPreviewHost")!;
|
||||
internal Border WallpaperPreviewFrame => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewFrame")!;
|
||||
internal Border WallpaperPreviewViewport => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewViewport")!;
|
||||
internal LibVLCSharp.Avalonia.VideoView? WallpaperPreviewVideoView => WallpaperSettingsPanel.FindControl<LibVLCSharp.Avalonia.VideoView>("WallpaperPreviewVideoView");
|
||||
internal Image WallpaperPreviewVideoImage => WallpaperSettingsPanel.FindControl<Image>("WallpaperPreviewVideoImage")!;
|
||||
internal Grid WallpaperPreviewGrid => WallpaperSettingsPanel.FindControl<Grid>("WallpaperPreviewGrid")!;
|
||||
internal Border WallpaperPreviewTopStatusBarHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewTopStatusBarHost")!;
|
||||
internal StackPanel WallpaperPreviewTopStatusComponentsPanel => WallpaperSettingsPanel.FindControl<StackPanel>("WallpaperPreviewTopStatusComponentsPanel")!;
|
||||
|
||||
@@ -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<Color> _recommendedColors = Array.Empty<Color>();
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
<Grid x:Name="WallpaperSettingsPanel"
|
||||
@@ -37,11 +36,12 @@
|
||||
CornerRadius="12"
|
||||
Background="#30111827">
|
||||
<Grid>
|
||||
<vlc:VideoView x:Name="WallpaperPreviewVideoView"
|
||||
IsVisible="False"
|
||||
IsHitTestVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch" />
|
||||
<Image x:Name="WallpaperPreviewVideoImage"
|
||||
IsVisible="False"
|
||||
IsHitTestVisible="False"
|
||||
Stretch="UniformToFill"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch" />
|
||||
|
||||
<Grid x:Name="WallpaperPreviewGrid"
|
||||
HorizontalAlignment="Center"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using LibVLCSharp.Avalonia;
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class SettingsWindow
|
||||
@@ -14,7 +13,7 @@ public partial class SettingsWindow
|
||||
internal Border WallpaperPreviewHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewHost")!;
|
||||
internal Border WallpaperPreviewFrame => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewFrame")!;
|
||||
internal Border WallpaperPreviewViewport => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewViewport")!;
|
||||
internal LibVLCSharp.Avalonia.VideoView? WallpaperPreviewVideoView => WallpaperSettingsPanel.FindControl<LibVLCSharp.Avalonia.VideoView>("WallpaperPreviewVideoView");
|
||||
internal Image WallpaperPreviewVideoImage => WallpaperSettingsPanel.FindControl<Image>("WallpaperPreviewVideoImage")!;
|
||||
internal Grid WallpaperPreviewGrid => WallpaperSettingsPanel.FindControl<Grid>("WallpaperPreviewGrid")!;
|
||||
internal Border WallpaperPreviewTopStatusBarHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewTopStatusBarHost")!;
|
||||
internal StackPanel WallpaperPreviewTopStatusComponentsPanel => WallpaperSettingsPanel.FindControl<StackPanel>("WallpaperPreviewTopStatusComponentsPanel")!;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Color> _recommendedColors = Array.Empty<Color>();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}'.");
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user