diff --git a/CollapseLauncher.slnx b/CollapseLauncher.slnx
index bfdd010376..04fa93e882 100644
--- a/CollapseLauncher.slnx
+++ b/CollapseLauncher.slnx
@@ -53,6 +53,10 @@
+
+
+
+
diff --git a/CollapseLauncher/App.xaml.cs b/CollapseLauncher/App.xaml.cs
index 327484fb9c..fdbbb153fb 100644
--- a/CollapseLauncher/App.xaml.cs
+++ b/CollapseLauncher/App.xaml.cs
@@ -137,12 +137,6 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
{
ImageLoaderHelper.InitWaifu2X();
}
-
- // Initialize support for MagicScaler's WebP decoding
- CodecManager.Configure(codecs =>
- {
- codecs.UseLibwebp();
- });
}
catch (Exception ex)
{
diff --git a/CollapseLauncher/Assets/Images/GameBackground/genshin.avif b/CollapseLauncher/Assets/Images/GameBackground/genshin.avif
new file mode 100644
index 0000000000..fca3d6690f
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameBackground/genshin.avif differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/genshin.webp b/CollapseLauncher/Assets/Images/GameBackground/genshin.webp
deleted file mode 100644
index 2b38257579..0000000000
Binary files a/CollapseLauncher/Assets/Images/GameBackground/genshin.webp and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/honkai.avif b/CollapseLauncher/Assets/Images/GameBackground/honkai.avif
new file mode 100644
index 0000000000..478fdfa9d6
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameBackground/honkai.avif differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/honkai.webp b/CollapseLauncher/Assets/Images/GameBackground/honkai.webp
deleted file mode 100644
index 8d0ed46ea8..0000000000
Binary files a/CollapseLauncher/Assets/Images/GameBackground/honkai.webp and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/starrail.avif b/CollapseLauncher/Assets/Images/GameBackground/starrail.avif
new file mode 100644
index 0000000000..54f1e52ec4
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameBackground/starrail.avif differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/starrail.webp b/CollapseLauncher/Assets/Images/GameBackground/starrail.webp
deleted file mode 100644
index 149b046deb..0000000000
Binary files a/CollapseLauncher/Assets/Images/GameBackground/starrail.webp and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/zzz.avif b/CollapseLauncher/Assets/Images/GameBackground/zzz.avif
new file mode 100644
index 0000000000..df987f585e
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameBackground/zzz.avif differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/zzz.webp b/CollapseLauncher/Assets/Images/GameBackground/zzz.webp
deleted file mode 100644
index d1f3f1a198..0000000000
Binary files a/CollapseLauncher/Assets/Images/GameBackground/zzz.webp and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_genshin.avif b/CollapseLauncher/Assets/Images/GamePoster/poster_genshin.avif
new file mode 100644
index 0000000000..a5868121d8
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GamePoster/poster_genshin.avif differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_genshin.png b/CollapseLauncher/Assets/Images/GamePoster/poster_genshin.png
deleted file mode 100644
index ee2504b7d2..0000000000
Binary files a/CollapseLauncher/Assets/Images/GamePoster/poster_genshin.png and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_honkai.avif b/CollapseLauncher/Assets/Images/GamePoster/poster_honkai.avif
new file mode 100644
index 0000000000..e4a7e00469
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GamePoster/poster_honkai.avif differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_honkai.png b/CollapseLauncher/Assets/Images/GamePoster/poster_honkai.png
deleted file mode 100644
index 3ba48393b9..0000000000
Binary files a/CollapseLauncher/Assets/Images/GamePoster/poster_honkai.png and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_starrail.avif b/CollapseLauncher/Assets/Images/GamePoster/poster_starrail.avif
new file mode 100644
index 0000000000..6d59581e3c
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GamePoster/poster_starrail.avif differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_starrail.png b/CollapseLauncher/Assets/Images/GamePoster/poster_starrail.png
deleted file mode 100644
index 9d82a6712f..0000000000
Binary files a/CollapseLauncher/Assets/Images/GamePoster/poster_starrail.png and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_zzz.avif b/CollapseLauncher/Assets/Images/GamePoster/poster_zzz.avif
new file mode 100644
index 0000000000..592af416a7
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GamePoster/poster_zzz.avif differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_zzz.png b/CollapseLauncher/Assets/Images/GamePoster/poster_zzz.png
deleted file mode 100644
index 389af86feb..0000000000
Binary files a/CollapseLauncher/Assets/Images/GamePoster/poster_zzz.png and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/StartupBackground.png b/CollapseLauncher/Assets/Images/PageBackground/StartupBackground.png
deleted file mode 100644
index 4cad046e38..0000000000
Binary files a/CollapseLauncher/Assets/Images/PageBackground/StartupBackground.png and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/StartupBackground2.avif b/CollapseLauncher/Assets/Images/PageBackground/StartupBackground2.avif
new file mode 100644
index 0000000000..1514071f5f
Binary files /dev/null and b/CollapseLauncher/Assets/Images/PageBackground/StartupBackground2.avif differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/StartupBackground2.png b/CollapseLauncher/Assets/Images/PageBackground/StartupBackground2.png
deleted file mode 100644
index 25f0a2a092..0000000000
Binary files a/CollapseLauncher/Assets/Images/PageBackground/StartupBackground2.png and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_genshin.avif b/CollapseLauncher/Assets/Images/PageBackground/default_genshin.avif
new file mode 100644
index 0000000000..82415e3a9f
Binary files /dev/null and b/CollapseLauncher/Assets/Images/PageBackground/default_genshin.avif differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_genshin.webp b/CollapseLauncher/Assets/Images/PageBackground/default_genshin.webp
deleted file mode 100644
index 30bbfa05aa..0000000000
Binary files a/CollapseLauncher/Assets/Images/PageBackground/default_genshin.webp and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_honkai.avif b/CollapseLauncher/Assets/Images/PageBackground/default_honkai.avif
new file mode 100644
index 0000000000..fcda9bf2a0
Binary files /dev/null and b/CollapseLauncher/Assets/Images/PageBackground/default_honkai.avif differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_honkai.webp b/CollapseLauncher/Assets/Images/PageBackground/default_honkai.webp
deleted file mode 100644
index 6953e43a31..0000000000
Binary files a/CollapseLauncher/Assets/Images/PageBackground/default_honkai.webp and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_starrail.avif b/CollapseLauncher/Assets/Images/PageBackground/default_starrail.avif
new file mode 100644
index 0000000000..241acde4b3
Binary files /dev/null and b/CollapseLauncher/Assets/Images/PageBackground/default_starrail.avif differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_starrail.webp b/CollapseLauncher/Assets/Images/PageBackground/default_starrail.webp
deleted file mode 100644
index c696b35089..0000000000
Binary files a/CollapseLauncher/Assets/Images/PageBackground/default_starrail.webp and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_zzz.avif b/CollapseLauncher/Assets/Images/PageBackground/default_zzz.avif
new file mode 100644
index 0000000000..6a325a280e
Binary files /dev/null and b/CollapseLauncher/Assets/Images/PageBackground/default_zzz.avif differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_zzz.webp b/CollapseLauncher/Assets/Images/PageBackground/default_zzz.webp
deleted file mode 100644
index 0e33010e68..0000000000
Binary files a/CollapseLauncher/Assets/Images/PageBackground/default_zzz.webp and /dev/null differ
diff --git a/CollapseLauncher/Classes/AnimatedVisuals/Lottie/NewLogoTitleIntro.cs b/CollapseLauncher/Classes/AnimatedVisuals/Lottie/NewLogoTitleIntro.cs
index bd524472c6..415c66c2c0 100644
--- a/CollapseLauncher/Classes/AnimatedVisuals/Lottie/NewLogoTitleIntro.cs
+++ b/CollapseLauncher/Classes/AnimatedVisuals/Lottie/NewLogoTitleIntro.cs
@@ -43,6 +43,9 @@
// | Gradient stops | 6 |
// | CompositionVisualSurface | 42 |
// ------------------------------------
+
+using CollapseLauncher.GameManagement.ImageBackground;
+using CollapseLauncher.Helper.Image;
using Hi3Helper.Shared.Region;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
@@ -55,6 +58,7 @@
using System.ComponentModel;
using System.IO;
using System.Numerics;
+using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI;
@@ -156,9 +160,9 @@ void EnsureImageLoadingStarted()
if (!_isImageLoadingStarted)
{
var eventHandler = new TypedEventHandler(HandleLoadCompleted);
- _image_image_0 = LoadedImageSurface.StartLoadFromStream(File.OpenRead(Path.Combine(LauncherConfig.AppExecutableDir, @"Assets\CollapseLauncherLogo.png")).AsRandomAccessStream());
+ _image_image_0 = LoadedImageSurface.StartLoadFromStream(File.OpenRead(Path.Combine(LauncherConfig.AppExecutableDir, @"Assets\CollapseLauncherLogo.png")).AsRandomAccessStream());
_image_image_0.LoadCompleted += eventHandler;
- _isImageLoadingStarted = true;
+ _isImageLoadingStarted = true;
}
}
diff --git a/CollapseLauncher/Classes/Extension/UIElementExtensions.cs b/CollapseLauncher/Classes/Extension/UIElementExtensions.cs
index 88e3c5a1b4..2214d2376f 100644
--- a/CollapseLauncher/Classes/Extension/UIElementExtensions.cs
+++ b/CollapseLauncher/Classes/Extension/UIElementExtensions.cs
@@ -148,14 +148,14 @@ internal static void SetAllControlsCursorRecursive(this UIElement? element, Inpu
}
}
- internal static void BindProperty(this FrameworkElement element,
- object? objectToBind,
- string propertyName,
- DependencyProperty dependencyProperty,
- BindingMode bindingMode,
- IValueConverter? converter = null)
- {
- element.SetBinding(dependencyProperty, new Binding
+ internal static void BindProperty(this FrameworkElement? element,
+ object? objectToBind,
+ string propertyName,
+ DependencyProperty dependencyProperty,
+ BindingMode bindingMode,
+ IValueConverter? converter = null)
+ {
+ element?.SetBinding(dependencyProperty, new Binding
{
Source = objectToBind,
Mode = bindingMode,
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ClipboardUtility.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ClipboardUtility.cs
index 40a7ac5db1..d42914b0d8 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ClipboardUtility.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ClipboardUtility.cs
@@ -1,5 +1,4 @@
-using CollapseLauncher.Extension;
-using CollapseLauncher.XAMLs.Theme.CustomControls;
+using CollapseLauncher.XAMLs.Theme.CustomControls;
using Hi3Helper;
using Hi3Helper.Win32.ManagedTools;
using Hi3Helper.Win32.Native.Enums;
@@ -8,13 +7,11 @@
using Hi3Helper.Win32.WinRT.WindowsStream;
using Microsoft.Extensions.Logging;
using Microsoft.Graphics.Canvas;
-using Microsoft.UI.Dispatching;
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
-using System.Threading;
using System.Threading.Tasks;
using Windows.Graphics.DirectX;
using Windows.Graphics.DirectX.Direct3D11;
@@ -30,10 +27,8 @@ public static class ClipboardUtility
{
public static async void CopyCurrentFrameToClipboard(
ImageBackgroundManager? instance,
- FrameToCopyType frameToCopyType)
+ FrameToCopyType frameToCopyType)
{
- bool isBackgroundVideo = false;
-
try
{
if (instance == null)
@@ -43,67 +38,14 @@ public static async void CopyCurrentFrameToClipboard(
bool hasBackground = frameToCopyType.HasFlag(FrameToCopyType.Background);
bool hasForeground = frameToCopyType.HasFlag(FrameToCopyType.Foreground);
- bool hasBoth = frameToCopyType == FrameToCopyType.Both;
// Try to get video frame buffer first (if the background is video).
LayeredBackgroundImage? currentElement = instance.CurrentBackgroundElement;
- bool isStaticBackgroundShowing = currentElement is { BackgroundStaticSource: not null, IsVideoPlay: false };
-
- string? usedBackgroundPath = isStaticBackgroundShowing
- ? instance.CurrentSelectedBackgroundContext?
- .OriginBackgroundStaticImagePath
- : instance.CurrentSelectedBackgroundContext?
- .OriginBackgroundImagePath;
-
- CanvasRenderTarget? backgroundCanvas = hasBackground
- ? isStaticBackgroundShowing ? null : currentElement?.LockCanvasRenderTarget()
- : null;
-
- ICanvasResourceCreator? canvasDevice = backgroundCanvas;
- canvasDevice ??= CanvasDevice.GetSharedDevice();
-
- if (backgroundCanvas != null)
- {
- // Copy bitmap to avoid arbitrary write to video frame buffer.
- CanvasRenderTarget oldCanvas = backgroundCanvas;
- backgroundCanvas = new CanvasRenderTarget(canvasDevice, backgroundCanvas.Size._width, backgroundCanvas.Size._height, 96f);
- backgroundCanvas.CopyPixelsFromBitmap(oldCanvas);
-
- isBackgroundVideo = true; // Mark as video so we can unlock the frame later.
- }
-
- // If it's still null (which most likely an image), get the canvas.
- backgroundCanvas ??= hasBackground
- ? await GetImageCanvasTargetBuffer(canvasDevice,
- usedBackgroundPath)
- : await GetImageCanvasTargetBuffer(canvasDevice,
- instance.CurrentSelectedBackgroundContext?
- .OriginOverlayImagePath);
-
- // If background canvas is still null, abort.
- if (backgroundCanvas == null)
- {
+ if (currentElement == null)
return;
- }
-
- // Get foreground canvas if available.
- CanvasRenderTarget? foregroundCanvas = hasBoth && hasForeground
- ? await GetImageCanvasTargetBuffer(canvasDevice,
- instance.CurrentSelectedBackgroundContext?
- .OriginOverlayImagePath)
- : null;
-
- int widthOfForeground = (int)(foregroundCanvas?.SizeInPixels.Width ?? 0u);
- int heightOfForeground = (int)(foregroundCanvas?.SizeInPixels.Height ?? 0u);
- if (foregroundCanvas != null &&
- (backgroundCanvas.SizeInPixels.Width != widthOfForeground ||
- backgroundCanvas.SizeInPixels.Height != heightOfForeground))
- {
- backgroundCanvas = ResizeCanvasToSizeOf(backgroundCanvas, widthOfForeground, heightOfForeground);
- }
- MergeCanvasTargetWith(backgroundCanvas, foregroundCanvas); // Merge overlay (foreground) to background.
- await CopyCanvasTargetBufferToClipboard(backgroundCanvas); // Copy to clipboard
+ using CanvasRenderTarget? sourceFrameCanvas = await currentElement.CopyCurrentFrame(hasBackground, hasForeground);
+ await CopyCanvasTargetBufferToClipboard(sourceFrameCanvas); // Copy to clipboard
}
catch (Exception ex)
{
@@ -111,89 +53,6 @@ public static async void CopyCurrentFrameToClipboard(
LogType.Error,
true);
}
- finally
- {
- if (isBackgroundVideo)
- {
- instance?.CurrentBackgroundElement?.UnlockCanvasRenderTarget();
- }
- }
- }
-
- private static CanvasRenderTarget ResizeCanvasToSizeOf(CanvasRenderTarget canvasToResize, int width, int height)
- {
- CanvasRenderTarget resizedCanvas = new(canvasToResize,
- width,
- height,
- canvasToResize.Dpi);
- using CanvasDrawingSession ds = resizedCanvas.CreateDrawingSession();
- ds.DrawImage(canvasToResize,
- new Windows.Foundation.Rect(0, 0, width, height),
- new Windows.Foundation.Rect(0, 0,
- canvasToResize.SizeInPixels.Width,
- canvasToResize.SizeInPixels.Height),
- 1f,
- CanvasImageInterpolation.HighQualityCubic);
-
- canvasToResize.Dispose();
- return resizedCanvas;
- }
-
- private static async Task GetImageCanvasTargetBuffer(ICanvasResourceCreator canvasDevice, string? path)
- {
- if (string.IsNullOrEmpty(path))
- {
- return null; // Ignore if path is null or empty.
- }
-
- // Get source image as native or decoded image (if it requires external codec decoding).
- Uri sourceUri = new(path);
- (sourceUri, _) = await ImageBackgroundManager
- .GetNativeOrDecodedImagePath(sourceUri, CancellationToken.None);
-
- using CanvasBitmap bitmap = sourceUri.IsFile ?
- await CanvasBitmap.LoadAsync(canvasDevice,
- sourceUri.LocalPath,
- 96f,
- CanvasAlphaMode.Premultiplied) :
- await CanvasBitmap.LoadAsync(canvasDevice,
- sourceUri,
- 96f,
- CanvasAlphaMode.Premultiplied);
-
- // Load to render target
- return await Task.Run(() =>
- {
- CanvasRenderTarget renderTarget = new(canvasDevice,
- bitmap.SizeInPixels.Width,
- bitmap.SizeInPixels.Height, bitmap.Dpi);
- using CanvasDrawingSession ds = renderTarget.CreateDrawingSession();
- ds.DrawImage(bitmap);
- return renderTarget;
- });
- }
-
- private static void MergeCanvasTargetWith(CanvasRenderTarget sourceCanvasTarget,
- CanvasRenderTarget? withCanvasTarget)
- {
- if (withCanvasTarget == null)
- {
- return; // Abort
- }
-
- CanvasDrawingSession? ds = sourceCanvasTarget.CreateDrawingSession();
- ds.DrawImage(withCanvasTarget);
-
- // Dispose the overlay canvas
- withCanvasTarget.Dispose();
-
- if (DispatcherQueueExtensions.CurrentDispatcherQueue.HasThreadAccess)
- {
- ds.Dispose();
- return;
- }
-
- DispatcherQueueExtensions.CurrentDispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, ds.Dispose);
}
private static double GetBitmapBitSize(DirectXPixelFormat pixelFormat)
@@ -300,9 +159,7 @@ private static async Task WriteCanvasTargetBufferType(
private static async Task CopyCanvasTargetBufferToClipboard(CanvasRenderTarget? sourceCanvasTarget)
{
if (sourceCanvasTarget == null)
- {
return;
- }
bool isClipboardOpened = true;
byte[]? buffer = null;
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs
index 1156e2df3a..ab907d7c89 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs
@@ -6,10 +6,11 @@
using System;
using System.IO;
using System.Threading.Tasks;
-
+// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable StringLiteralTypo
// ReSharper disable CheckNamespace
+#pragma warning disable IDE0130
#nullable enable
namespace CollapseLauncher.GameManagement.ImageBackground;
@@ -18,72 +19,60 @@ public partial class ImageBackgroundManager
{
#region Codec Checks
- private async ValueTask<(bool IsSupported, bool IsVideo, bool ForceFFmpeg)> CheckCodecOrSpawnDialog(Uri? fileUri)
+ private async ValueTask<(bool IsSupported, bool ForceFFmpeg)> CheckCodecOrSpawnDialog(string? localPath)
{
- // -- Cancel if null or URI is not a local file
- if (fileUri == null || !fileUri.IsFile)
- {
- return (false, false, false);
- }
-
- string filePath = fileUri.LocalPath;
+ if (localPath == null)
+ return (false, false);
// -- Check for supported extension first
- if (!IsMediaFileExtensionSupported(filePath))
+ if (!IsMediaFileExtensionSupported(localPath))
{
- await SimpleDialogs.Dialog_SpawnMediaExtensionNotSupportedDialog(filePath);
- return (false, false, false);
+ await SimpleDialogs.Dialog_SpawnMediaExtensionNotSupportedDialog(localPath);
+ return (false, false);
}
// -- Check for supported image codec
- if (IsImageMediaFileExtensionSupported(filePath))
+ if (IsImageMediaFileExtensionSupported(localPath))
{
- if (WindowsCodecHelper.IsFileSupportedImage(filePath))
+ if (WindowsCodecHelper.IsFileSupportedImage(localPath))
{
- return (true, false, false);
+ return (true, false);
}
- await SimpleDialogs.Dialog_SpawnImageNotSupportedDialog(filePath);
- return (false, false, false);
+ await SimpleDialogs.Dialog_SpawnImageNotSupportedDialog(localPath);
+ return (false, false);
}
// -- Check using FFmpeg if available
- MediaSupport codecInfo = FFmpegPInvoke.GetMediaSupport(filePath);
+ MediaSupport codecInfo = FFmpegPInvoke.GetMediaSupport(localPath);
+ PixelColorModel pixelFormat = codecInfo.VideoPixelFormatInfo.ColorModel;
if ((GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable) ||
- RequiresFFmpegDecoder(codecInfo))
+ (codecInfo.Video.DecoderAvailable && pixelFormat.HasFlag(PixelColorModel.Rgb)))
{
- return (true, true, true);
+ return (true, true);
}
// -- Check for supported video codec
- if (WindowsCodecHelper.IsFileSupportedVideo(filePath,
+ if (WindowsCodecHelper.IsFileSupportedVideo(localPath,
out bool canPlayVideo,
out bool canPlayAudio,
out Guid videoCodecGuid,
out Guid audioCodecGuid) ||
(GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable))
{
- return (true, true, true);
+ return (true, false);
}
return (await SimpleDialogs
- .Dialog_SpawnVideoNotSupportedDialog(filePath,
+ .Dialog_SpawnVideoNotSupportedDialog(localPath,
canPlayVideo,
canPlayAudio,
videoCodecGuid,
- audioCodecGuid), true, false);
+ audioCodecGuid), false);
}
#endregion
- private static bool RequiresFFmpegDecoder(MediaSupport codecInfo)
- {
- // Windows' H.264 decoder does not support the High 10 profile used by some backgrounds.
- return codecInfo.Video.DecoderAvailable &&
- (codecInfo.VideoPixelFormatInfo.ColorModel.HasFlag(PixelColorModel.Rgb) ||
- codecInfo.VideoPixelFormatInfo.Format is AVPixelFormat.AV_PIX_FMT_YUV420P10LE or AVPixelFormat.AV_PIX_FMT_YUV420P10BE);
- }
-
private static bool IsMediaFileExtensionSupported(ReadOnlySpan filePath)
{
return IsImageMediaFileExtensionSupported(filePath) ||
@@ -98,7 +87,7 @@ private static bool IsImageMediaFileExtensionSupported(ReadOnlySpan filePa
LayeredBackgroundImage.SupportedImageVectorExtensionsLookup.Contains(extension);
}
- private static bool IsVideoMediaFileExtensionSupported(ReadOnlySpan filePath)
+ public static bool IsVideoMediaFileExtensionSupported(ReadOnlySpan filePath)
{
ReadOnlySpan extension = Path.GetExtension(filePath);
return LayeredBackgroundImage.SupportedVideoExtensionsLookup.Contains(extension);
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Control.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Control.cs
index 58f6c6ab97..1492aae22f 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Control.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Control.cs
@@ -1,7 +1,4 @@
-using CollapseLauncher.XAMLs.Theme.CustomControls;
-using System.Threading;
-
-// ReSharper disable ConvertIfStatementToSwitchStatement
+// ReSharper disable ConvertIfStatementToSwitchStatement
// ReSharper disable CheckNamespace
#pragma warning disable IDE0130
@@ -10,61 +7,56 @@ namespace CollapseLauncher.GameManagement.ImageBackground;
public partial class ImageBackgroundManager
{
- private bool _isPausedByUser;
-
public void SetWindowFocusedEvent()
{
- if (GlobalKeepPlayVideoWhenWindowUnfocused)
- {
- CurrentBackgroundElement?.FadeInAudio();
- }
- else
+ if (!GlobalKeepPlayVideoWhenWindowUnfocused)
{
Play(false);
+ return;
}
+
+ CurrentBackgroundElement?.SetVolume(GlobalBackgroundAudioVolume / 100d);
}
public void SetWindowUnfocusedEvent()
{
- if (GlobalKeepPlayVideoWhenWindowUnfocused)
- {
- CurrentBackgroundElement?.FadeOutAudio();
- }
- else
+ if (!GlobalKeepPlayVideoWhenWindowUnfocused)
{
Pause(false);
+ return;
}
+
+ CurrentBackgroundElement?.SetVolume(0);
}
- public void SetWindowMinimizeEvent() => Pause(false);
+ public void SetWindowMinimizeEvent()
+ {
+ CurrentBackgroundElement?.ReleaseViewAllocation();
+ }
- public void SetWindowRestoreEvent() => Play(false);
+ public void SetWindowRestoreEvent()
+ {
+ // Trynna spawn the background element if not loaded from first start-up.
+ if (CurrentBackgroundElement == null)
+ LoadImage();
+
+ CurrentBackgroundElement?.RestoreViewAllocation();
+ }
public void Play(bool isUserRequest = true)
{
- // Block play request for window event if paused by user
- if (!isUserRequest && (_isPausedByUser || !CurrentIsEnableBackgroundAutoPlay))
- {
- return;
- }
-
if (isUserRequest)
{
CurrentIsEnableBackgroundAutoPlay = true;
}
-
- // Alter to load static image if it has one instead of pausing.
- LayeredImageBackgroundContext? context = CurrentSelectedBackgroundContext;
- bool hasStaticImage = !string.IsNullOrEmpty(context?.BackgroundImageStaticPath);
- if (isUserRequest && hasStaticImage && context != null)
+
+ // Alter to restore video if it has static image.
+ if (isUserRequest && !string.IsNullOrEmpty(CurrentSelectedBackgroundContext?.OriginPlaceholderImagePath))
{
- LoadImageAtIndex(CurrentSelectedBackgroundIndex, false, CancellationToken.None, true);
+ LoadImage();
+ return;
}
- // Force to restore autoplay status to true.
- CurrentBackgroundElement?.SetValue(LayeredBackgroundImage.IsVideoAutoplayProperty, true);
-
- Interlocked.Exchange(ref _isPausedByUser, false);
CurrentBackgroundElement?.Play();
}
@@ -72,16 +64,13 @@ public void Pause(bool isUserRequest = true)
{
if (isUserRequest)
{
- Interlocked.Exchange(ref _isPausedByUser, true);
CurrentIsEnableBackgroundAutoPlay = false;
}
-
+
// Alter to load static image if it has one instead of pausing.
- LayeredImageBackgroundContext? context = CurrentSelectedBackgroundContext;
- bool hasStaticImage = !string.IsNullOrEmpty(context?.BackgroundImageStaticPath);
- if (isUserRequest && hasStaticImage && context != null)
+ if (isUserRequest && !string.IsNullOrEmpty(CurrentSelectedBackgroundContext?.OriginPlaceholderImagePath))
{
- LoadImageAtIndex(CurrentSelectedBackgroundIndex, true, CancellationToken.None, true);
+ LoadImage();
return;
}
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.ImageCropperAndConvert.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.ImageCropperAndConvert.cs
index 3cb1b575be..6e16b4acd7 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.ImageCropperAndConvert.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.ImageCropperAndConvert.cs
@@ -1,6 +1,7 @@
using CollapseLauncher.Dialogs;
using CollapseLauncher.Extension;
using CollapseLauncher.Helper;
+using CollapseLauncher.Helper.Image;
using CollapseLauncher.Helper.StreamUtility;
using CollapseLauncher.XAMLs.Theme.ContentDialog;
using CollapseLauncher.XAMLs.Theme.CustomControls;
@@ -35,6 +36,39 @@ namespace CollapseLauncher.GameManagement.ImageBackground;
public partial class ImageBackgroundManager
{
+ private readonly struct GetCroppedResult
+ {
+ public readonly string? ProcessedOverlayPath;
+ public readonly string? ProcessedBackgroundPath;
+ public readonly bool IsCancelResize;
+ public readonly bool WasCropPerformed;
+
+ private GetCroppedResult(string? processedOverlayPath,
+ string? processedBackgroundPath,
+ bool isCancelResize,
+ bool wasCropPerformed)
+ {
+ ProcessedOverlayPath = processedOverlayPath;
+ ProcessedBackgroundPath = processedBackgroundPath;
+ IsCancelResize = isCancelResize;
+ WasCropPerformed = wasCropPerformed;
+ }
+
+ public static GetCroppedResult Create(string? processedOverlayPath,
+ string? processedBackgroundPath,
+ bool isCancelResize,
+ bool wasCropPerformed)
+ {
+ return new GetCroppedResult(processedOverlayPath,
+ processedBackgroundPath,
+ isCancelResize,
+ wasCropPerformed);
+ }
+
+ public static readonly GetCroppedResult Cancelled = Create(null, null, true, false);
+ public static readonly GetCroppedResult Skip = Create(null, null, false, false);
+ }
+
///
/// Get local path of the resized/cropped custom image. The return values are cropped image path.
/// If the input path is a video, then it will return the input path.
@@ -42,7 +76,7 @@ public partial class ImageBackgroundManager
/// To perform cropping request, you must set to .
/// Otherwise, it will return the input paths.
///
- private static async Task<(string? ProcessedOverlayPath, string? ProcessedBackgroundPath, bool IsCancelResize)>
+ private static async Task
GetCroppedCustomImage(string? overlayUrlOrPath,
string? backgroundUrlOrPath,
bool performCropRequest,
@@ -52,21 +86,21 @@ public partial class ImageBackgroundManager
string.IsNullOrEmpty(backgroundUrlOrPath)) ||
string.IsNullOrEmpty(backgroundUrlOrPath))
{
- return (null, null, false); // Skip from processing
+ return GetCroppedResult.Skip;
}
// -- Check if the file is video or image
if (LayeredBackgroundImage.SupportedVideoExtensionsLookup
.Contains(Path.GetExtension(backgroundUrlOrPath)))
{
- return (overlayUrlOrPath, backgroundUrlOrPath, false);
+ return GetCroppedResult.Create(overlayUrlOrPath, backgroundUrlOrPath, false, false);
}
// -- Try to get existing image file path
if (TryGetCroppedImageFilePath(backgroundUrlOrPath, out string croppedBackgroundPath) &&
!performCropRequest)
{
- return (null, croppedBackgroundPath, false);
+ return GetCroppedResult.Create(null, croppedBackgroundPath, false, false);
}
try
@@ -90,26 +124,21 @@ public partial class ImageBackgroundManager
Uri backgroundImageUri = new(decodedBackgroundPath);
// -- Create elements, load image and spawn the dialog box.
- Grid parentGrid = UIElementExtensions
- .Create(SetParentGridProperties);
-
- ContentDialogOverlay dialog = UIElementExtensions
- .Create(SetDialogProperties);
-
- ImageCropper cropper = UIElementExtensions
- .Create(SetImageCropperProperties);
+ Grid parentGrid = UIElementExtensions.Create(SetParentGridProperties);
+ ContentDialogOverlay dialog = UIElementExtensions.Create(SetDialogProperties);
+ ImageCropper cropper = UIElementExtensions.Create(SetImageCropperProperties);
// Skip spawning crop overlay when source dimensions do not meet minimum crop size.
if (!await CanOpenCropOverlay(backgroundImageUri, backgroundCodecType == ImageExternalCodecType.Svg, cropper, token))
{
- return (overlayUrlOrPath, backgroundUrlOrPath, false);
+ return GetCroppedResult.Create(overlayUrlOrPath, backgroundUrlOrPath, false, false);
}
// -- Register to close dialog if cancellation is triggered outside the event.
token.Register(dialog.Hide);
if (token.IsCancellationRequested)
{
- return (null, null, true);
+ return GetCroppedResult.Cancelled;
}
DispatcherQueueExtensions
@@ -124,29 +153,18 @@ public partial class ImageBackgroundManager
ContentDialogResult dialogResult = await dialog.QueueAndSpawnDialog();
if (dialogResult is ContentDialogResult.Secondary or ContentDialogResult.None)
{
- return (null, null, true);
+ return GetCroppedResult.Cancelled;
}
// -- Save cropped image
await SaveCroppedImageToFilePath(backgroundUrlOrPath, croppedBackgroundPath, cropper, token);
// -- If there's an upscaled image, delete it.
- if (TryGetUpscaledFilePath(croppedBackgroundPath,
- out string croppedImagePath))
- {
- try
- {
- File.Delete(croppedImagePath);
- }
- catch (Exception ex)
- {
- Logger.LogWriteLine($"An error has occurred while trying to delete previous upscaled image: {ex}",
- LogType.Error,
- true);
- }
- }
+ if (TryGetUpscaledFilePath(croppedBackgroundPath, out string croppedImagePath))
+ StreamExtension.TryDeleteFile(croppedImagePath);
+
- return (null, croppedBackgroundPath, false);
+ return GetCroppedResult.Create(null, croppedBackgroundPath, false, true);
void SetDialogProperties(ContentDialogOverlay element)
{
@@ -189,7 +207,73 @@ void SetImageCropperProperties(ImageCropper element)
}
catch (OperationCanceledException)
{
- return (null, null, true);
+ return GetCroppedResult.Cancelled;
+ }
+ }
+
+ public static bool TryGetUpscaledFilePath(string inputFilePath,
+ out string upscaledFilePath)
+ {
+ upscaledFilePath = Path.Combine(LauncherConfig.AppGameImgCachedFolder, $"scaled_{Path.GetFileNameWithoutExtension(inputFilePath)}.png");
+ if (LayeredBackgroundImage.SupportedVideoExtensionsLookup.Contains(Path.GetExtension(inputFilePath))) // Ignore if file is a video.
+ {
+ upscaledFilePath = inputFilePath;
+ return true;
+ }
+
+ FileInfo outputFileInfo = new(upscaledFilePath);
+ return outputFileInfo is { Exists: true, Length: > 0 };
+ }
+
+ public static async Task TryGetScaledWaifu2XImagePath(Uri uri, CancellationToken token)
+ {
+ if (uri == null!)
+ {
+ return null!;
+ }
+
+ if (!uri.IsFile)
+ {
+ throw new InvalidOperationException("You must provide a local file path!");
+ }
+
+ // Ignore if the input file is a video
+ string inputFilePath = uri.LocalPath;
+ if (TryGetUpscaledFilePath(inputFilePath, out string outputFilePath))
+ {
+ return new Uri(outputFilePath);
+ }
+
+ FileInfo outputFileInfo = new(outputFilePath);
+ outputFileInfo.Directory?.Create();
+
+ await Task.Run(Impl, token);
+ outputFileInfo.Refresh();
+
+ try
+ {
+ token.ThrowIfCancellationRequested();
+ return new Uri(outputFileInfo.FullName);
+ }
+ catch
+ {
+ outputFileInfo.TryDeleteFile();
+ throw;
+ }
+
+ void Impl()
+ {
+ using ProcessingPipeline pipeline =
+ MagicImageProcessor
+ .BuildPipeline(inputFilePath,
+ ProcessImageSettings.Default);
+
+ using FileStream outputFileStream = outputFileInfo.Open(FileMode.Create,
+ FileAccess.Write,
+ FileShare.ReadWrite);
+
+ pipeline.AddTransform(new Waifu2XTransform(ImageLoaderHelper.Waifu2X));
+ pipeline.WriteOutput(outputFileStream);
}
}
@@ -382,7 +466,7 @@ private static async Task ConvertImageToPngFromStream(
///
/// Converts any (seekable) image stream to PNG.
///
- private static Task ConvertImageToPngFromStream(
+ private static async Task ConvertImageToPngFromStream(
Stream sourceStream,
Stream targetStream,
Rectangle cropArea = default,
@@ -394,7 +478,8 @@ private static Task ConvertImageToPngFromStream(
IsBackground = true
}.Start((sourceStream, targetStream, tcs, token, cropArea));
- return tcs.Task;
+ await tcs.Task;
+ return;
static void Impl(object? state)
{
@@ -407,6 +492,8 @@ static void Impl(object? state)
try
{
+ innerSourceStream.Position = 0;
+
// We won't do any post-processing and just do a straight conversion to PNG.
// The post-processing (like resizing with Waifu2X) will be done on image loading process.
ProcessImageSettings settings = new()
@@ -478,32 +565,43 @@ private static bool IsWebpExtendedFormat(Stream stream, out ColorProfileMode col
/// Get native or redirected decoded image path.
///
/// If the codec of the image is not supported.
- internal static async ValueTask<(string, ImageExternalCodecType)> GetNativeOrDecodedImagePath(string filePath, CancellationToken token)
+ internal static ValueTask<(string, ImageExternalCodecType)> GetNativeOrDecodedImagePath(string filePath, CancellationToken token = default)
{
// Try to get decoded temporary file. If it exists, return the file path.
if (TryGetDecodedTemporaryFile(filePath, out string decodedFilePath))
{
- return (decodedFilePath, ImageExternalCodecType.Default);
+ return new ValueTask<(string, ImageExternalCodecType)>((decodedFilePath, ImageExternalCodecType.Default));
}
- // Check the extension type. If the type is native or a video file, then just return the original path.
- if (await TryGetImageCodecType(filePath, token) is var codecType &&
- codecType is ImageExternalCodecType.Default or ImageExternalCodecType.Svg)
+ Task<(string, ImageExternalCodecType)> processTask = ImplAsync();
+ if (processTask.IsCompleted)
{
- return (filePath, codecType);
+ return new ValueTask<(string, ImageExternalCodecType)>(processTask.Result);
}
- // Return null if the codec type is not supported.
- if (codecType == ImageExternalCodecType.NotSupported)
+ return new ValueTask<(string, ImageExternalCodecType)>(processTask);
+
+ async Task<(string, ImageExternalCodecType)> ImplAsync()
{
- throw new NotSupportedException($"The format of the image: {filePath} is not supported!");
- }
+ // Check the extension type. If the type is native or a video file, then just return the original path.
+ if (await TryGetImageCodecType(filePath, token) is var codecType &&
+ codecType is ImageExternalCodecType.Default or ImageExternalCodecType.Svg)
+ {
+ return (filePath, codecType);
+ }
- // Otherwise, try to convert the image to png and write it to given decoded file path.
- await using Stream sourceStream = await OpenStreamFromFileOrUrl(filePath, token);
- await ConvertImageToPngFromStream(sourceStream, decodedFilePath, token: token);
+ // Return null if the codec type is not supported.
+ if (codecType == ImageExternalCodecType.NotSupported)
+ {
+ throw new NotSupportedException($"The format of the image: {filePath} is not supported!");
+ }
+
+ // Otherwise, try to convert the image to png and write it to given decoded file path.
+ await using Stream sourceStream = await OpenStreamFromFileOrUrl(filePath, token);
+ await ConvertImageToPngFromStream(sourceStream, decodedFilePath, token: token);
- return (decodedFilePath, codecType);
+ return (decodedFilePath, codecType);
+ }
}
///
@@ -511,7 +609,7 @@ private static bool IsWebpExtendedFormat(Stream stream, out ColorProfileMode col
///
/// If the codec of the image is not supported.
internal static async ValueTask<(Uri, ImageExternalCodecType)> GetNativeOrDecodedImagePath(
- Uri filePath, CancellationToken token)
+ Uri filePath, CancellationToken token = default)
{
(string, ImageExternalCodecType) result = await GetNativeOrDecodedImagePath(filePath.ToString(), token);
return (new Uri(result.Item1), result.Item2);
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs
index 003848bca3..1c93090f12 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs
@@ -1,35 +1,21 @@
-using CollapseLauncher.Extension;
+using CollapseLauncher.Extension;
using CollapseLauncher.Helper;
using CollapseLauncher.Helper.Background;
-using CollapseLauncher.Helper.Image;
-using CollapseLauncher.Helper.InternalPInvoke;
-using CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
-using CollapseLauncher.Helper.Metadata;
-using CollapseLauncher.Helper.StreamUtility;
using CollapseLauncher.Pages;
using CollapseLauncher.XAMLs.Theme.CustomControls;
using Hi3Helper;
using Hi3Helper.SentryHelper;
using Hi3Helper.Shared.Region;
-using Microsoft.UI.Xaml;
-using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Data;
-using Microsoft.UI.Xaml.Media;
-using Microsoft.UI.Xaml.Media.Animation;
-using Microsoft.UI.Xaml.Media.Imaging;
-using PhotoSauce.MagicScaler;
using System;
-using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Windows.UI;
// ReSharper disable IdentifierTypo
// ReSharper disable CheckNamespace
+// ReSharper disable CommentTypo
#nullable enable
namespace CollapseLauncher.GameManagement.ImageBackground;
@@ -40,643 +26,255 @@ public partial class ImageBackgroundManager
private CancellationTokenSource? _imageLoadingTokenSource;
+ private static readonly MinMaxToPercentConverter MinMaxToPercentConverter = new(0d, 100d);
+
+ private string? _lastBackgroundSource;
+
#endregion
- private void LoadImageAtIndex(int index, bool forceLoadToStatic, CancellationToken token, bool forceReload = false)
+ private void LoadImage(CancellationToken token = default)
{
- if (ImageContextSources.Count <= index ||
- index < 0)
+ if (ImageContextSources.Count == 0 ||
+ CurrentSelectedBackgroundIndex < 0 ||
+ PresenterGrid == null)
{
return;
}
- if (IsBackgroundLoading)
- {
- CancelCurrentBackgroundLoad();
- }
+ new Thread(Impl) { IsBackground = true }.Start();
+ return;
- // Only show loading indicator when a download is required.
- if (!CheckIsContextCached(ImageContextSources[index]))
- {
- IsBackgroundLoading = true;
- }
- new Thread(async void () =>
+ async void Impl()
{
+ Stopwatch? stopwatch = null;
try
{
- await LoadImageAtIndexCore(index, forceLoadToStatic, token, forceReload).ConfigureAwait(false);
- }
- catch (Exception e)
- {
- Logger.LogWriteLine($"{e}", LogType.Error, true);
- }
- })
- {
- IsBackground = true,
- Priority = ThreadPriority.Lowest
- }.UnsafeStart();
- }
+ // -- Get context and invalidate previous CTS
+ LayeredImageBackgroundContext? context = CurrentSelectedBackgroundContext;
+ if (context == null)
+ return;
- private void CancelCurrentBackgroundLoad()
- {
- if (Interlocked.Exchange(ref _imageLoadingTokenSource, null) is { } lastCts)
- {
- lastCts.Cancel();
- lastCts.Dispose();
- }
- IsBackgroundLoading = false;
- }
-
- public void PreloadBackground(PresetConfig? preset, Grid? presenterGrid)
- {
- if (preset == null || presenterGrid == null) return;
-
- CancelCurrentBackgroundLoad();
- Interlocked.Increment(ref _loadGeneration);
-
- PresenterGrid = presenterGrid;
- string autoPlayKey = $"LastIsEnableBackgroundAutoPlay-{preset.GameName}-{preset.ZoneName}";
- CurrentIsEnableBackgroundAutoPlayKey = autoPlayKey;
-
- string cachedBgKey = $"CachedBg-{preset.GameName}-{preset.ZoneName}";
- string? backgroundUrl = LauncherConfig.GetAppConfigValue($"{cachedBgKey}-Background").ToString();
- string? staticBgUrl = LauncherConfig.GetAppConfigValue($"{cachedBgKey}-StaticBackground").ToString();
- string? overlayUrl = LauncherConfig.GetAppConfigValue($"{cachedBgKey}-Overlay").ToString();
-
- if (string.IsNullOrEmpty(backgroundUrl) && string.IsNullOrEmpty(staticBgUrl))
- {
- IsBackgroundLoading = true;
- return;
- }
+ // -- Invalidate last Cancellation Token (and thus, cancel all asynchronous routines)
+ // and then renew the token. Also toggle the background loading indicator.
+ DispatcherQueueExtensions.CurrentDispatcherQueue.TryEnqueue(() => IsBackgroundLoading = true );
+ token = InvalidateAndRenewCancelToken(token);
- string? primaryBg = backgroundUrl ?? staticBgUrl;
+ stopwatch = Stopwatch.StartNew();
- LayeredImageBackgroundContext context = new()
- {
- OriginOverlayImagePath = overlayUrl,
- OriginBackgroundImagePath = backgroundUrl,
- OriginBackgroundStaticImagePath = staticBgUrl,
- OverlayImagePath = overlayUrl,
- BackgroundImagePath = primaryBg,
- BackgroundImageStaticPath = staticBgUrl,
- IsVideo = IsVideoMediaFileExtensionSupported(primaryBg ?? staticBgUrl!),
- IsCustom = false
- };
-
- if (CheckIsContextCached(context))
- {
- string? localOverlay = TryGetCachedLocalPath(overlayUrl);
- string? localBackground = TryGetCachedLocalPath(primaryBg);
- string? localStatic = TryGetCachedLocalPath(staticBgUrl);
+ // -- Notify changes on context menu properties
+ DispatcherQueueExtensions.CurrentDispatcherQueue.TryEnqueue(() => OnPropertyChanged(nameof(CurrentSelectedBackgroundHasOverlayImage)));
- bool overlayRequired = !string.IsNullOrEmpty(overlayUrl);
- bool staticRequired = !string.IsNullOrEmpty(staticBgUrl);
+ // -- Start clean-up if force reload requested
+ if (context.ForceReload)
+ {
+ context.DeleteCachedImagePath(context.OriginOverlayImagePath);
+ context.DeleteCachedImagePath(context.OriginBackgroundImagePath);
+ context.DeleteCachedImagePath(context.OriginPlaceholderImagePath);
+ }
- if (localBackground != null &&
- (!overlayRequired || localOverlay != null) &&
- (!staticRequired || localStatic != null))
- {
- bool willDisplayStatic = (!IsVideoMediaFileExtensionSupported(primaryBg ?? staticBgUrl!) ||
- (!CurrentIsEnableCustomImage &&
- !GlobalIsEnableCustomImage &&
- !CurrentIsEnableBackgroundAutoPlay))
- && localStatic != null;
- Uri accentPreviewUri = willDisplayStatic ? new Uri(localStatic!) : new Uri(localBackground);
+ // -- Get Cached path.
+ string? downloadedOverlayUri = null;
+ string? downloadedBackgroundUri = null;
+ string? downloadedPlaceholderUri = null;
- _ = RestoreSavedAccent(cachedBgKey, accentPreviewUri);
+ Task overlayLoadTask = context.GetCachedImagePathAsync(context.OriginOverlayImagePath, token).GetResultFromAction(result => downloadedOverlayUri = result);
+ Task backgroundLoadTask = context.GetCachedImagePathAsync(context.OriginBackgroundImagePath, token).GetResultFromAction(result => downloadedBackgroundUri = result);
+ Task placeholderLoadTask = context.GetCachedImagePathAsync(context.OriginPlaceholderImagePath, token).GetResultFromAction(result => downloadedPlaceholderUri = result);
- ImageContextSources.Clear();
- ImageContextSources.Add(context);
- OnPropertyChanged(nameof(CurrentSelectedBackgroundContext));
- OnPropertyChanged(nameof(CurrentBackgroundCount));
+ await Task.WhenAll(overlayLoadTask,
+ backgroundLoadTask,
+ placeholderLoadTask);
- bool isUseFFmpeg = GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable;
- bool isVideo = IsVideoMediaFileExtensionSupported(primaryBg ?? staticBgUrl!);
+ token.ThrowIfCancellationRequested();
- MediaSupport codecInfo = isVideo ? FFmpegPInvoke.GetMediaSupport(localBackground) : default;
- if (isUseFFmpeg || RequiresFFmpegDecoder(codecInfo))
+ if (!string.IsNullOrEmpty(CurrentCachedBgKey))
{
- isUseFFmpeg = true;
+ LauncherConfig.SetAndSaveConfigValue($"{CurrentCachedBgKey}-Background", context.OriginBackgroundImagePath ?? "");
+ LauncherConfig.SetAndSaveConfigValue($"{CurrentCachedBgKey}-StaticBackground", context.OriginPlaceholderImagePath ?? "");
+ LauncherConfig.SetAndSaveConfigValue($"{CurrentCachedBgKey}-Overlay", context.OriginOverlayImagePath ?? "");
}
- CurrentBackgroundElement = CreateLayerElement(localOverlay != null ? new Uri(localOverlay) : null,
- new Uri(localBackground),
- localStatic != null ? new Uri(localStatic) : null,
- context,
- isVideo,
- isUseFFmpeg);
- new Thread(async void (ctx) =>
+ // -- Use UI thread and load image layer
+ DispatcherQueueExtensions
+ .CurrentDispatcherQueue
+ .TryEnqueue(DispatcherQueuePriority.High,
+ AssignSource);
+
+ return;
+
+ async void AssignSource()
{
try
{
- await GetMediaAccentColor(ctx).ConfigureAwait(false);
+ string? backgroundToUse = (CurrentIsEnableBackgroundAutoPlay ? downloadedBackgroundUri : downloadedPlaceholderUri) ?? downloadedBackgroundUri;
+
+ // -- Avoid spawning element while window is currently not visible
+ // (for example: Minimized at start).
+ if (!WindowUtility.CurrentWindowIsVisible)
+ {
+ return;
+ }
+
+ // -- Avoid the same background to get loaded (unless Force reload is requested).
+ if (_lastBackgroundSource == backgroundToUse && !context.ForceReload)
+ {
+ return;
+ }
+
+ // -- Update last background source.
+ _lastBackgroundSource = backgroundToUse;
+
+ // -- Reset force reload request.
+ bool forceReloadRequested = context.ForceReload;
+ context.ForceReload = false;
+
+ // -- Check media support and whether to force FFmpeg in some cases.
+ (bool isSupported, bool forceFFmpeg) = await CheckCodecOrSpawnDialog(backgroundToUse);
+ if (!isSupported)
+ {
+ return;
+ }
+
+ // -- Notify whether the background is a video or not.
+ CurrentBackgroundIsVideo = context.IsVideo;
+
+ // -- Create or Get element and set background.
+ LayeredBackgroundImage element = CreateOrGetLayerElement();
+ bool useFFmpeg = forceFFmpeg || (context.IsVideo && GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable);
+
+ // -- If Force Reload previously requested, rapidly nullify the background source.
+ if (forceReloadRequested)
+ element.BackgroundSource = null;
+
+ element.UseFfmpegDecoder = useFFmpeg;
+ element.BackgroundSource = backgroundToUse;
+ element.ForegroundSource = downloadedOverlayUri;
+
+ // -- Load color accent
+ if (!string.IsNullOrEmpty(backgroundToUse))
+ LoadMediaAccentColor(new Uri(backgroundToUse), useFFmpeg);
}
catch (Exception e)
{
- Logger.LogWriteLine($"{e}", LogType.Error, true);
+ Console.WriteLine(e);
}
- })
- {
- IsBackground = true
- }.UnsafeStart((accentPreviewUri, isUseFFmpeg, cachedBgKey));
- return;
- }
- }
-
- // Show preset placeholder.
- string placeholderPath = GetPlaceholderBackgroundImageFrom(null);
- PresenterGrid.Background = new ImageBrush
- {
- ImageSource = new BitmapImage(new Uri(placeholderPath)),
- Stretch = Stretch.UniformToFill
- };
- CurrentBackgroundElement = null;
- _displayedContext = null;
- IsBackgroundLoading = true;
- }
-
- private async Task LoadImageAtIndexCore(int index, bool forceLoadToStatic, CancellationToken token, bool forceReload = false)
- {
- Stopwatch? stopwatch = null;
- try
- {
- bool isUseFFmpeg = GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable;
- stopwatch = Stopwatch.StartNew();
-
- // -- Notify changes on context menu properties
- DispatcherQueueExtensions
- .CurrentDispatcherQueue
- .TryEnqueue(() => OnPropertyChanged(nameof(CurrentSelectedBackgroundHasOverlayImage)));
-
- // -- Get context and invalidate previous CTS
- LayeredImageBackgroundContext context = ImageContextSources[index];
- if (Interlocked.Exchange(ref _imageLoadingTokenSource,
- CancellationTokenSource.CreateLinkedTokenSource(token)) is { } lastCts)
- {
- await lastCts.CancelAsync();
- lastCts.Dispose();
- }
-
- token = _imageLoadingTokenSource.Token;
-
- // -- Download overlay.
- Unsafe.SkipInit(out Uri? downloadedOverlayUri);
- if (Uri.TryCreate(context.OverlayImagePath, UriKind.Absolute, out Uri? overlayImageUri))
- {
- downloadedOverlayUri = await GetLocalOrDownloadedFilePath(overlayImageUri, token).ConfigureAwait(false);
- (downloadedOverlayUri, _) = await GetNativeOrDecodedImagePath(downloadedOverlayUri, token).ConfigureAwait(false);
- }
-
- // -- Download background.
- Unsafe.SkipInit(out Uri? downloadedBackgroundUri);
- if (Uri.TryCreate(context.BackgroundImagePath, UriKind.Absolute, out Uri? backgroundImageUri))
- {
- downloadedBackgroundUri = await GetLocalOrDownloadedFilePath(backgroundImageUri, token).ConfigureAwait(false);
- (downloadedBackgroundUri, _) = await GetNativeOrDecodedImagePath(downloadedBackgroundUri, token).ConfigureAwait(false);
- }
-
- // -- Download static background.
- Unsafe.SkipInit(out Uri? downloadedBackgroundStaticUri);
- if (Uri.TryCreate(context.BackgroundImageStaticPath, UriKind.Absolute, out Uri? backgroundStaticImageUri))
- {
- downloadedBackgroundStaticUri = await GetLocalOrDownloadedFilePath(backgroundStaticImageUri, token).ConfigureAwait(false);
- (downloadedBackgroundStaticUri, _) = await GetNativeOrDecodedImagePath(downloadedBackgroundStaticUri, token).ConfigureAwait(false);
- }
-
- // Try to use static bg URL if normal bg is not available.
- downloadedBackgroundUri ??= downloadedBackgroundStaticUri;
- if (downloadedBackgroundUri == null)
- {
- if (!context.IsCustom)
- {
- NeedFallbackPlaceholder = false;
- OnBackgroundLoadFailed?.Invoke();
}
- return;
}
-
- token.ThrowIfCancellationRequested();
-
- // -- Check for codec support (Also spawn dialog to install either native WIC/MediaFoundation decoder or using Ffmpeg decoder)
- (bool isSupported, bool isVideo, bool forceFFmpeg) = await CheckCodecOrSpawnDialog(downloadedBackgroundUri);
- if (!isSupported) return;
-
- // -- Force the use of FFmpeg if necessary.
- if (forceFFmpeg) isUseFFmpeg = true;
-
- // -- Get upscaled image file if Waifu2X is enabled
- if (GlobalIsWaifu2XEnabled && !isVideo)
+ catch (Exception ex) when (ex is not OperationCanceledException)
{
- downloadedOverlayUri = await TryGetScaledWaifu2XImagePath(downloadedOverlayUri, token).ConfigureAwait(false);
- downloadedBackgroundUri = await TryGetScaledWaifu2XImagePath(downloadedBackgroundUri, token).ConfigureAwait(false);
- downloadedBackgroundStaticUri = await TryGetScaledWaifu2XImagePath(downloadedBackgroundStaticUri, token).ConfigureAwait(false);
+ _ = SentryHelper.ExceptionHandlerAsync(ex);
+ Logger.LogWriteLine($"[ImageBackgroundManager::LoadImage] {ex}",
+ LogType.Error,
+ true);
}
-
- // Try to force loading static image if requested.
- if (forceLoadToStatic && downloadedBackgroundStaticUri != null)
+ catch (OperationCanceledException)
{
- isVideo = false;
- downloadedBackgroundUri = downloadedBackgroundStaticUri;
- downloadedBackgroundStaticUri = null;
+ // Suppress: cancellation is expected on game switch or rapid load cycles.
}
-
- // Read accent color from the source that is actually displayed.
- bool willDisplayStatic = !isVideo ||
- (!CurrentIsEnableCustomImage &&
- !GlobalIsEnableCustomImage &&
- !CurrentIsEnableBackgroundAutoPlay);
- Uri? accentSourceUri = willDisplayStatic
- ? downloadedBackgroundStaticUri ?? downloadedBackgroundUri
- : downloadedBackgroundUri;
-
- new Thread(async void (ctx) =>
+ finally
{
- try
- {
- await GetMediaAccentColor(ctx).ConfigureAwait(false);
- }
- catch (Exception e)
- {
- Logger.LogWriteLine($"{e}", LogType.Error, true);
- }
- })
- {
- IsBackground = true
- }.UnsafeStart((accentSourceUri, isUseFFmpeg, CurrentCachedBgKey));
-
- if (!context.IsCustom &&
- !string.IsNullOrEmpty(CurrentCachedBgKey))
- {
- LauncherConfig.SetAndSaveConfigValue($"{CurrentCachedBgKey}-Background", context.OriginBackgroundImagePath ?? "");
- LauncherConfig.SetAndSaveConfigValue($"{CurrentCachedBgKey}-StaticBackground", context.OriginBackgroundStaticImagePath ?? "");
- LauncherConfig.SetAndSaveConfigValue($"{CurrentCachedBgKey}-Overlay", context.OriginOverlayImagePath ?? "");
- }
+ stopwatch?.Stop();
+ Logger.LogWriteLine($"Background image loading took: {stopwatch?.Elapsed.TotalSeconds} second(s)");
- // -- Use UI thread and load image layer
- int captureGen = _loadGeneration;
- bool captureForceReload = forceReload;
- DispatcherQueueExtensions
- .CurrentDispatcherQueue
- .TryEnqueue(() => SpawnImageLayer(downloadedOverlayUri,
- downloadedBackgroundUri,
- downloadedBackgroundStaticUri,
- isVideo,
- forceFFmpeg,
- context,
- captureGen,
- forceReload: captureForceReload));
-
- GlobalIsFFmpegCurrentlyUsed = isVideo && isUseFFmpeg;
- }
- catch (Exception ex) when (ex is not OperationCanceledException)
- {
- _ = SentryHelper.ExceptionHandlerAsync(ex);
- Logger.LogWriteLine($"[ImageBackgroundManager::LoadImageAtIndex] {ex}",
- LogType.Error,
- true);
- }
- catch (OperationCanceledException)
- {
- // Suppress: cancellation is expected on game switch or rapid load cycles.
- }
- finally
- {
- stopwatch?.Stop();
- Logger.LogWriteLine($"Background image loading took: {stopwatch?.Elapsed.TotalSeconds} second(s)");
- if (!token.IsCancellationRequested)
- {
- IsBackgroundLoading = false;
+ DispatcherQueueExtensions.CurrentDispatcherQueue.TryEnqueue(() => IsBackgroundLoading = false);
}
}
}
- private void SpawnImageLayer(Uri? overlayFilePath,
- Uri? backgroundFilePath,
- Uri? backgroundStaticFilePath,
- bool isVideo,
- bool forceFFmpeg,
- LayeredImageBackgroundContext context,
- int loadGeneration,
- bool forceReload = false)
+ private CancellationToken InvalidateAndRenewCancelToken(CancellationToken coopToken = default)
{
- if (loadGeneration != _loadGeneration)
- {
- return;
- }
+ CancellationTokenSource newCts;
+ CancellationTokenSource? lastCts =
+ Interlocked.Exchange(ref _imageLoadingTokenSource,
+ newCts = CancellationTokenSource.CreateLinkedTokenSource(coopToken));
- if (!forceReload &&
- CurrentBackgroundElement != null &&
- CurrentBackgroundElement.UseFfmpegDecoder == (forceFFmpeg || (GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable)) &&
- _displayedContext != null)
- {
- if (context.Equals(_displayedContext))
- {
- return;
- }
+ lastCts?.Cancel();
+ lastCts?.Dispose();
- if (CurrentBackgroundElement is { } existingLayer &&
- IsSameLocalFile(existingLayer.BackgroundSource, backgroundFilePath) &&
- IsSameLocalFile(existingLayer.BackgroundStaticSource, backgroundStaticFilePath))
- {
- return;
- }
- }
-
- CurrentBackgroundElement = CreateLayerElement(overlayFilePath, backgroundFilePath, backgroundStaticFilePath, context, isVideo, forceFFmpeg);
- }
-
- private static bool IsSameLocalFile(object? currentSource, Uri? newFilePath)
- {
- if (newFilePath == null) return currentSource == null;
- string newPath = newFilePath.IsFile ? newFilePath.LocalPath : newFilePath.OriginalString;
- string? currentPath = currentSource switch
- {
- Uri uri => uri.IsFile ? uri.LocalPath : uri.OriginalString,
- string str => str,
- _ => null
- };
- return string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase);
+ return newCts.Token;
}
- private async Task RestoreSavedAccent(string cachedBgKey, Uri? fallbackSourceUri = null)
+ private LayeredBackgroundImage CreateOrGetLayerElement()
{
- string? savedHex = LauncherConfig.GetAppConfigValue($"{cachedBgKey}-AccentColor").ToString();
- if (!string.IsNullOrEmpty(savedHex) && savedHex.Length >= 6 && ThemeRootElement != null)
+ if (CurrentBackgroundElement == null)
{
- if (TryParseHexColor(savedHex, out Color accentColor))
+ CurrentBackgroundElement = new LayeredBackgroundImage
{
- ThemeRootElement.ChangeAccentColor(accentColor);
- return;
- }
- }
-
- if (fallbackSourceUri != null && ThemeRootElement != null)
- {
- try
- {
- Color syncColor = await ColorPaletteUtility.GetMediaAccentColorFromAsync(fallbackSourceUri, false);
- ThemeRootElement.ChangeAccentColor(syncColor);
- LauncherConfig.SetAndSaveConfigValue($"{cachedBgKey}-AccentColor",
- $"{syncColor.R:X2}{syncColor.G:X2}{syncColor.B:X2}");
- }
- catch (Exception ex)
- {
- Logger.LogWriteLine($"RestoreSavedAccent failed: {ex}", LogType.Error, true);
- }
- }
- }
-
- private static bool TryParseHexColor(ReadOnlySpan hex, out Color color)
- {
- color = default;
- if (hex.Length < 6) return false;
- if (hex[0] == '#')
- hex = hex[1..];
- if (!uint.TryParse(hex, NumberStyles.HexNumber, null, out uint argb)) return false;
- if (hex.Length <= 6)
- argb = 0xFF_000000 | argb;
- color = Color.FromArgb((byte)(argb >> 24), (byte)(argb >> 16), (byte)(argb >> 8), (byte)argb);
- return true;
- }
-
- private LayeredBackgroundImage CreateLayerElement(Uri? overlayFilePath,
- Uri? backgroundFilePath,
- Uri? backgroundStaticFilePath,
- LayeredImageBackgroundContext context,
- bool isVideo,
- bool forceFFmpeg)
- {
- LayeredBackgroundImage layerElement = new()
- {
- Opacity = 0,
- BackgroundSource = backgroundFilePath,
- BackgroundStaticSource = backgroundStaticFilePath,
- ForegroundSource = isVideo ? overlayFilePath : null,
- UseFfmpegDecoder = forceFFmpeg || (GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable),
- Tag = context,
- ParallaxResetOnUnfocused = false,
- BackgroundElevationPixels = 64d
- };
-
- if (!CurrentIsEnableCustomImage &&
- !GlobalIsEnableCustomImage &&
- backgroundStaticFilePath != null)
- {
- layerElement.BindProperty(LayeredBackgroundImage.IsVideoAutoplayProperty,
- this,
- nameof(CurrentIsEnableBackgroundAutoPlay),
- bindingMode: BindingMode.OneWay,
- converter: StaticConverter.Shared);
- }
- else
- {
- layerElement.IsVideoAutoplay = WindowUtility.CurrentWindowIsVisible;
- }
-
- layerElement.BindProperty(LayeredBackgroundImage.FfmpegDecoderModeProperty,
- this,
- nameof(GlobalFFmpegDecodingMode),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.ParallaxHoverSourceProperty,
- this,
- nameof(GlobalParallaxHoverSource),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.IsParallaxEnabledProperty,
- this,
- nameof(GlobalIsBackgroundParallaxEffectEnabled),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.ParallaxHorizontalShiftProperty,
- this,
- nameof(GlobalBackgroundParallaxPixelShift),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.ParallaxVerticalShiftProperty,
- this,
- nameof(GlobalBackgroundParallaxPixelShift),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.IsAudioEnabledProperty,
- this,
- nameof(GlobalBackgroundAudioEnabled),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.AudioVolumeProperty,
- this,
- nameof(GlobalBackgroundAudioVolume),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.IsBackgroundElevatedProperty,
- this,
- nameof(IsBackgroundElevated),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.ForegroundOpacityProperty,
- this,
- nameof(ForegroundOpacity),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.SmokeOpacityProperty,
- this,
- nameof(SmokeOpacity),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.UseImageCacheProperty,
- context,
- nameof(context.ForceReload),
- bindingMode: BindingMode.OneWay,
- converter: StaticConverter.Shared);
-
- layerElement.ImageLoaded += LayerElementOnLoaded;
- PresenterGrid?.Children.Add(layerElement);
-
- layerElement.Tag = isVideo;
- _displayedContext = context;
- return layerElement;
- }
-
- private void LayerElementOnLoaded(LayeredBackgroundImage layerElement)
- {
- layerElement.ImageLoaded -= LayerElementOnLoaded;
- layerElement.Transitions.Add(new PopupThemeTransition());
-
- if (PresenterGrid != null)
- {
- List toRemove = PresenterGrid.Children.Where(element => element != layerElement).ToList();
- foreach (UIElement oldElement in toRemove)
- {
- if (oldElement is LayeredBackgroundImage oldLayer)
- {
- oldLayer.ImageLoaded -= LayerElementOnLoaded;
-
- if (oldLayer.Tag == null)
- {
- oldLayer.Opacity = 0;
- }
- else
- {
- PresenterGrid.Children.Remove(oldElement);
- }
- }
- else
- {
- PresenterGrid.Children.Remove(oldElement);
- }
- }
-
- PresenterGrid.Background = null;
- }
-
- layerElement.Opacity = 1;
-
- if (CurrentIsEnableBackgroundAutoPlay && WindowUtility.CurrentWindowIsVisible)
- {
- Play(false);
+ ParallaxResetOnUnfocused = false,
+ BackgroundElevationPixels = 64d
+ };
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.FfmpegDecoderModeProperty,
+ this,
+ nameof(GlobalFFmpegDecodingMode),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.ParallaxHoverSourceProperty,
+ this,
+ nameof(GlobalParallaxHoverSource),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.IsParallaxEnabledProperty,
+ this,
+ nameof(GlobalIsBackgroundParallaxEffectEnabled),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.ParallaxHorizontalShiftProperty,
+ this,
+ nameof(GlobalBackgroundParallaxPixelShift),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.ParallaxVerticalShiftProperty,
+ this,
+ nameof(GlobalBackgroundParallaxPixelShift),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.IsAudioEnabledProperty,
+ this,
+ nameof(GlobalBackgroundAudioEnabled),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.AudioVolumeProperty,
+ this,
+ nameof(GlobalBackgroundAudioVolume),
+ bindingMode: BindingMode.OneWay,
+ converter: MinMaxToPercentConverter);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.IsBackgroundElevatedProperty,
+ this,
+ nameof(IsBackgroundElevated),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.ForegroundOpacityProperty,
+ this,
+ nameof(ForegroundOpacity),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.SmokeOpacityProperty,
+ this,
+ nameof(SmokeOpacity),
+ bindingMode: BindingMode.OneWay);
+
+ PresenterGrid?.Children.Clear();
+ PresenterGrid?.Children.Add(CurrentBackgroundElement);
}
- CurrentBackgroundIsVideo = CurrentSelectedBackgroundContext?.IsVideo ?? false;
- if (layerElement.Tag is bool isDisplayControl)
- {
- CurrentBackgroundIsSeekable = isDisplayControl;
- }
+ return CurrentBackgroundElement;
}
- private static bool TryGetUpscaledFilePath(string inputFilePath,
- out string upscaledFilePath)
- {
- upscaledFilePath = Path.Combine(LauncherConfig.AppGameImgCachedFolder, $"scaled_{Path.GetFileNameWithoutExtension(inputFilePath)}.png");
- if (LayeredBackgroundImage.SupportedVideoExtensionsLookup.Contains(Path.GetExtension(inputFilePath))) // Ignore if file is a video.
- {
- upscaledFilePath = inputFilePath;
- return true;
- }
-
- FileInfo outputFileInfo = new(upscaledFilePath);
- return outputFileInfo is { Exists: true, Length: > 0 };
- }
-
- private static async Task TryGetScaledWaifu2XImagePath(Uri? uri, CancellationToken token)
- {
- if (uri == null)
- {
- return null;
- }
-
- if (!uri.IsFile)
- {
- throw new InvalidOperationException("You must provide a local file path!");
- }
-
- // Ignore if the input file is a video
- string inputFilePath = uri.LocalPath;
- if (TryGetUpscaledFilePath(inputFilePath, out string outputFilePath))
- {
- return new Uri(outputFilePath);
- }
-
- FileInfo outputFileInfo = new(outputFilePath);
- outputFileInfo.Directory?.Create();
-
- await Task.Run(Impl, token);
- outputFileInfo.Refresh();
-
- try
- {
- token.ThrowIfCancellationRequested();
- return new Uri(outputFileInfo.FullName);
- }
- catch
- {
- outputFileInfo.TryDeleteFile();
- throw;
- }
-
- void Impl()
- {
- using ProcessingPipeline pipeline =
- MagicImageProcessor
- .BuildPipeline(inputFilePath,
- ProcessImageSettings.Default);
-
- using FileStream outputFileStream = outputFileInfo.Open(FileMode.Create,
- FileAccess.Write,
- FileShare.ReadWrite);
-
- pipeline.AddTransform(new Waifu2XTransform(ImageLoaderHelper.Waifu2X));
- pipeline.WriteOutput(outputFileStream);
- }
- }
-
- private async Task GetMediaAccentColor(object? context)
+ private async void LoadMediaAccentColor(Uri mediaUri, bool useFFmpeg)
{
try
{
- if (context is not (Uri asUri, bool useFfmpegForVideo, string configKey))
- {
- return;
- }
-
- Color color = await ColorPaletteUtility.GetMediaAccentColorFromAsync(asUri, useFfmpegForVideo)
+ Color color = await ColorPaletteUtility.GetMediaAccentColorFromAsync(mediaUri, useFFmpeg)
.ConfigureAwait(false);
- if (color == default) return;
-
- string hex = $"{color.R:X2}{color.G:X2}{color.B:X2}";
- if (!string.IsNullOrEmpty(configKey))
- {
- string? savedHex = LauncherConfig.GetAppConfigValue($"{configKey}-AccentColor").ToString();
- if (savedHex == hex) return;
- LauncherConfig.SetAndSaveConfigValue($"{configKey}-AccentColor", hex);
- }
-
ColorAccentChanged?.Invoke(color);
}
catch (Exception ex)
{
- Logger.LogWriteLine($"GetMediaAccentColor failed: {ex}", LogType.Error, true);
+ Logger.LogWriteLine($"LoadMediaAccentColor failed: {ex}", LogType.Error, true);
SentryHelper.ExceptionHandler(ex);
}
}
-}
+}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.StreamAndPath.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.StreamAndPath.cs
index be30af72a9..1af6185487 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.StreamAndPath.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.StreamAndPath.cs
@@ -1,5 +1,6 @@
using CollapseLauncher.Helper.Metadata;
using CollapseLauncher.Helper.StreamUtility;
+using Hi3Helper.Data;
using Hi3Helper.EncTool;
using Hi3Helper.Shared.Region;
using System;
@@ -21,38 +22,47 @@ public partial class ImageBackgroundManager
#region Fields
private static readonly string[] PlaceholderBackgroundImageRelPath = [
- @"Assets\Images\PageBackground\default_genshin.webp",
- @"Assets\Images\PageBackground\default_honkai.webp",
- @"Assets\Images\PageBackground\default_starrail.webp",
- @"Assets\Images\PageBackground\default_zzz.webp",
- ];
-
- private static readonly string[] PlaceholderBackgroundImagePngRelPath = [
- @"Assets\Images\GamePoster\poster_genshin.png",
- @"Assets\Images\GamePoster\poster_honkai.png",
- @"Assets\Images\GamePoster\poster_starrail.png",
- @"Assets\Images\GamePoster\poster_zzz.png",
+ @"Assets\Images\PageBackground\default_genshin.avif",
+ @"Assets\Images\PageBackground\default_honkai.avif",
+ @"Assets\Images\PageBackground\default_starrail.avif",
+ @"Assets\Images\PageBackground\default_zzz.avif"
];
#endregion
- private static ValueTask OpenStreamFromFileOrUrl(string? filePath, CancellationToken token)
+ public static ValueTask OpenStreamFromFileOrUrl(string? filePath, CancellationToken token)
{
- return !Uri.TryCreate(filePath, UriKind.Absolute, out Uri? uri)
+ return !Uri.TryCreate(filePath, UriKind.RelativeOrAbsolute, out Uri? uri)
? throw new InvalidOperationException($"File path or URL is misformed! {filePath}")
: OpenStreamFromFileOrUrl(uri, token);
}
- private static async ValueTask OpenStreamFromFileOrUrl(Uri uri, CancellationToken token)
+ public static async ValueTask OpenStreamFromFileOrUrl(Uri uri, CancellationToken token)
{
+ if (!uri.IsAbsoluteUri && !uri.IsWellFormedOriginalString())
+ {
+ string localPath = uri.OriginalString.GetFullyQualifiedPath();
+ return !File.Exists(localPath)
+ ? throw new FileNotFoundException($"File: {localPath} does not exist!")
+ : File.Open(localPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ }
+
if (uri.IsFile)
{
- string localPath = uri.LocalPath;
+ string localPath = uri.LocalPath.GetFullyQualifiedPath();
return !File.Exists(localPath)
? throw new FileNotFoundException($"File: {localPath} does not exist!")
: File.Open(localPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
+ if (uri.Scheme.StartsWith("ms-appx"))
+ {
+ string uwpResPath = uri.LocalPath.TrimStart("\\/").GetFullyQualifiedPath();
+ return !File.Exists(uwpResPath)
+ ? throw new FileNotFoundException($"File: {uwpResPath} does not exist!")
+ : File.Open(uwpResPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ }
+
// Find stamp file. Try read the metadata in it and just use locally cached files if matches.
if (TryGetDownloadedFile(uri,
out FileInfo downloadedFilePath,
@@ -66,11 +76,6 @@ private static async ValueTask OpenStreamFromFileOrUrl(Uri uri, Canc
status = await sharedClient.GetCachedUrlStatus(uri, token).ConfigureAwait(false);
status.EnsureSuccessStatusCode();
- if (status.FileSize == 0)
- {
- throw new HttpRequestException($"File URL is reachable but it returns 0 bytes. {uri}");
- }
-
downloadedFilePath = downloadedFilePath
.EnsureCreationOfDirectory()
.EnsureNoReadOnly();
@@ -116,7 +121,7 @@ internal static async ValueTask GetLocalOrDownloadedFilePath(string path
: uri.ToString();
}
- private static bool TryGetCroppedImageFilePath(string filePath,
+ internal static bool TryGetCroppedImageFilePath(string filePath,
out string croppedFilePath)
{
string imageDirPath = LauncherConfig.AppGameImgFolder;
@@ -126,8 +131,8 @@ private static bool TryGetCroppedImageFilePath(string filePath,
return IsFileExistAndNotEmpty(croppedFilePath);
}
- private static bool TryGetDecodedTemporaryFile(string filePath,
- out string decodedFilePath)
+ internal static bool TryGetDecodedTemporaryFile(string filePath,
+ out string decodedFilePath)
{
string imageDirPath = LauncherConfig.AppGameImgFolder;
string fileName = $"decoded_{Path.GetFileNameWithoutExtension(filePath)}.png";
@@ -142,10 +147,10 @@ private static bool IsFileExistAndNotEmpty(string filePath)
return fileInfo is { Exists: true, Length: > 0 };
}
- private static bool TryGetDownloadedFile(Uri filePath,
- out FileInfo downloadedFilePath,
- out FileInfo downloadStampFilePath,
- out UrlStatus urlStatusStamp)
+ internal static bool TryGetDownloadedFile(Uri filePath,
+ out FileInfo downloadedFilePath,
+ out FileInfo downloadStampFilePath,
+ out UrlStatus urlStatusStamp)
{
string imageDirPath = LauncherConfig.AppGameImgFolder;
string fileHashedName = $"{Path.GetFileName(filePath.AbsolutePath)}";
@@ -185,68 +190,24 @@ private static unsafe bool TryReadDownloadStampFile(
return true;
}
- internal static string GetPlaceholderBackgroundImageFrom(PresetConfig? presetConfig, bool usePngVersion = false)
+ internal static string GetPlaceholderBackgroundImageFrom(PresetConfig? presetConfig)
{
- string[] source = usePngVersion
- ? PlaceholderBackgroundImagePngRelPath
- : PlaceholderBackgroundImageRelPath;
-
- string relPath = presetConfig?.GameName ??
- LauncherConfig.GetAppConfigValue("GameCategory").Value switch
+ string[] source = PlaceholderBackgroundImageRelPath;
+ string relPath = presetConfig?.GameName switch
{
"Genshin Impact" => source[0],
"Honkai Impact 3rd" => source[1],
"Honkai: Star Rail" => source[2],
"Zenless Zone Zero" => source[3],
- _ => GetRandomPlaceholderImage(usePngVersion)
+ _ => GetRandomPlaceholderImage()
};
return Path.Combine(LauncherConfig.AppExecutableDir, relPath);
}
- public static string GetRandomPlaceholderImage(bool usePngVersion = false)
- {
- string[] source = usePngVersion
- ? PlaceholderBackgroundImagePngRelPath
- : PlaceholderBackgroundImageRelPath;
-
- return source[Random.Shared.Next(0, source.Length - 1)];
- }
-
- internal static bool CheckIsContextCached(LayeredImageBackgroundContext ctx)
- {
- bool backgroundCached = false;
- if (!string.IsNullOrEmpty(ctx.BackgroundImagePath) &&
- TryGetDownloadedFile(new Uri(ctx.BackgroundImagePath), out _, out _, out _))
- {
- backgroundCached = true;
- }
- else if (!string.IsNullOrEmpty(ctx.BackgroundImageStaticPath) &&
- TryGetDownloadedFile(new Uri(ctx.BackgroundImageStaticPath), out _, out _, out _))
- {
- backgroundCached = true;
- }
-
- return backgroundCached;
- }
-
- internal static string? TryGetCachedLocalPath(string? urlOrPath)
+ public static string GetRandomPlaceholderImage()
{
- if (string.IsNullOrEmpty(urlOrPath))
- {
- return null;
- }
-
- if (TryGetDecodedTemporaryFile(urlOrPath, out string decodedFilePath))
- {
- return decodedFilePath;
- }
-
- if (TryGetDownloadedFile(new Uri(urlOrPath), out FileInfo downloadedFile, out _, out _))
- {
- return downloadedFile.FullName;
- }
-
- return null;
+ string[] source = PlaceholderBackgroundImageRelPath;
+ return source[new Random(Environment.TickCount).Next(0, source.Length - 1)];
}
}
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.cs
index 0af623e028..b229bca895 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.cs
@@ -2,6 +2,7 @@
using CollapseLauncher.Helper.Image;
using CollapseLauncher.Helper.LauncherApiLoader.HoYoPlay;
using CollapseLauncher.Helper.Metadata;
+using CollapseLauncher.Helper.StreamUtility;
using CollapseLauncher.Interfaces;
using CollapseLauncher.Interfaces.Class;
using CollapseLauncher.XAMLs.Theme.CustomControls;
@@ -13,12 +14,14 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Windows.UI;
using WinRT;
+// ReSharper disable NonReadonlyMemberInGetHashCode
// ReSharper disable CheckNamespace
#pragma warning disable IDE0130
@@ -199,8 +202,6 @@ public double SmokeOpacity
private Grid? PresenterGrid { get; set; }
private PresetConfig? PresetConfig { get; set; }
- internal bool NeedFallbackPlaceholder { get; set; }
-
public Action? OnBackgroundLoadFailed { get; set; }
public string? CurrentCustomBackgroundImagePath
@@ -257,7 +258,7 @@ public int CurrentSelectedBackgroundIndex
LauncherConfig.SetAndSaveConfigValue(CurrentSelectedBackgroundIndexKey, value);
OnPropertyChanged();
- LoadImageAtIndex(value, false, CancellationToken.None, true);
+ LoadImage();
}
}
@@ -287,16 +288,6 @@ private set
public int CurrentBackgroundCount => ImageContextSources.Count;
- public bool CurrentBackgroundIsSeekable
- {
- get;
- private set
- {
- field = value;
- OnPropertyChanged();
- }
- }
-
public bool CurrentBackgroundIsVideo
{
get;
@@ -321,10 +312,6 @@ public ObservableCollection ImageContextSources
private int _previousContextHash;
- private volatile int _loadGeneration;
-
- private LayeredImageBackgroundContext? _displayedContext;
-
internal FrameworkElement? ThemeRootElement { get; set; }
#endregion
@@ -383,6 +370,7 @@ async void Impl()
// -- Try to initialize custom image first
// -- A. Check for per-region custom background
if (CurrentIsEnableCustomImage &&
+ !string.IsNullOrEmpty(CurrentCustomBackgroundImagePath) &&
await SetCurrentCustomBackground(CurrentCustomBackgroundImagePath, false, false, token))
{
return;
@@ -391,6 +379,7 @@ await SetCurrentCustomBackground(CurrentCustomBackgroundImagePath, false, false,
// -- B. Check for global custom background
if (!CurrentIsEnableCustomImage &&
GlobalIsEnableCustomImage &&
+ !string.IsNullOrEmpty(GlobalCustomBackgroundImagePath) &&
await SetGlobalCustomBackground(GlobalCustomBackgroundImagePath, false, false, token))
{
return;
@@ -408,8 +397,6 @@ await SetGlobalCustomBackground(GlobalCustomBackgroundImagePath, false, false, t
imageContexts.Add(new LayeredImageBackgroundContext
{
OriginBackgroundImagePath = bgPlaceholderPath,
- BackgroundImagePath = bgPlaceholderPath,
- IsVideo = IsVideoMediaFileExtensionSupported(bgPlaceholderPath),
IsCustom = true
});
return;
@@ -419,20 +406,15 @@ await SetGlobalCustomBackground(GlobalCustomBackgroundImagePath, false, false, t
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (HypLauncherBackgroundContentKindData contextEntry in contextList.SelectMany(x => x.Backgrounds))
{
- string? overlayImagePath = contextEntry.BackgroundOverlay?.ImageUrl;
- string? backgroundImagePath = contextEntry.BackgroundVideo?.ImageUrl ??
- contextEntry.BackgroundImage?.ImageUrl;
- string? backgroundImageStaticPath = contextEntry.BackgroundImage?.ImageUrl;
+ string? overlayImagePath = contextEntry.BackgroundOverlay?.ImageUrl;
+ string? backgroundImagePath = contextEntry.BackgroundVideo?.ImageUrl ?? contextEntry.BackgroundImage?.ImageUrl;
+ string? placeholderImagePath = contextEntry.BackgroundImage?.ImageUrl;
imageContexts.Add(new LayeredImageBackgroundContext
{
- OriginOverlayImagePath = overlayImagePath,
- OriginBackgroundImagePath = backgroundImagePath,
- OriginBackgroundStaticImagePath = backgroundImageStaticPath,
- OverlayImagePath = overlayImagePath,
- BackgroundImagePath = backgroundImagePath,
- BackgroundImageStaticPath = backgroundImageStaticPath,
- IsVideo = IsVideoMediaFileExtensionSupported(backgroundImagePath)
+ OriginOverlayImagePath = overlayImagePath,
+ OriginBackgroundImagePath = backgroundImagePath,
+ OriginPlaceholderImagePath = placeholderImagePath
});
}
}
@@ -485,14 +467,13 @@ private async Task SetCustomBackgroundCore(string? imagePath,
}
// -- Try to perform cropped or pass through processing
- (_, string? resultBackgroundPath, bool isCancelProcess) =
- await GetCroppedCustomImage(null,
- imagePath,
- performCropRequest,
- token);
+ GetCroppedResult cropResult = await GetCroppedCustomImage(null,
+ imagePath,
+ performCropRequest,
+ token);
// -- Do not process custom background if cancelled.
- if (isCancelProcess)
+ if (cropResult.IsCancelResize)
{
return false;
}
@@ -507,11 +488,8 @@ await GetCroppedCustomImage(null,
// -- Apply background
LayeredImageBackgroundContext context = new()
{
- OriginBackgroundImagePath = imagePath,
- BackgroundImagePath = resultBackgroundPath,
- ForceReload = true, // Request force reload (skip cache)
- IsVideo = IsVideoMediaFileExtensionSupported(resultBackgroundPath),
- IsCustom = true
+ OriginBackgroundImagePath = cropResult.ProcessedBackgroundPath,
+ ForceReload = cropResult.WasCropPerformed // Only do force reload if crop was performed.
};
UpdateContextListCore(token, skipPreviousContextCheck, context);
@@ -538,33 +516,14 @@ private void UpdateContextListCore(
return;
}
- UpdateContextListCore(token, skipPreviousContextCheck, imageContexts.ToArray());
+ UpdateContextListCore(token, skipPreviousContextCheck, [.. imageContexts]);
}
private void UpdateContextListCore(
CancellationToken token,
- bool skipPreviousContextCheck,
+ bool forceReload,
params ReadOnlySpan imageContexts)
{
- UpdateContextListCore(token, skipPreviousContextCheck, false, imageContexts);
- }
-
- private void UpdateContextListCore(
- CancellationToken token,
- bool skipPreviousContextCheck,
- bool forceLoadToStatic,
- params ReadOnlySpan imageContexts)
- {
- // Do not update if the previous contexts are equal
- if ((!skipPreviousContextCheck &&
- IsContextEqual(imageContexts)) ||
- imageContexts.IsEmpty)
- {
- return;
- }
-
- Interlocked.Increment(ref _loadGeneration);
-
ImageContextSources.Clear(); // Flush list
ref IList? backedList =
ref ObservableCollectionExtension
@@ -591,32 +550,14 @@ ref ObservableCollectionExtension
OnPropertyChanged(nameof(CurrentSelectedBackgroundIndex));
OnPropertyChanged(nameof(CurrentSelectedBackgroundContext));
OnPropertyChanged(nameof(CurrentBackgroundCount));
- LoadImageAtIndex(CurrentSelectedBackgroundIndex, forceLoadToStatic, token);
+ LoadImage(token);
}
#pragma warning restore CA1068
- private bool IsContextEqual(ReadOnlySpan imageContexts)
- {
- if (ImageContextSources.Count != imageContexts.Length)
- {
- return false;
- }
-
- HashCode hashCode = default;
- foreach (LayeredImageBackgroundContext context in imageContexts)
- {
- hashCode.Add(context);
- }
-
- int hashCodeRes = hashCode.ToHashCode();
- return Interlocked.Exchange(ref _previousContextHash, hashCodeRes) == hashCodeRes;
- }
-
public void ResetContexts()
{
Interlocked.Exchange(ref _previousContextHash, 0);
ImageContextSources.Clear();
- NeedFallbackPlaceholder = false;
}
void INotifyAllPropertyChanged.NotifyAllChanged()
@@ -638,49 +579,79 @@ public string? OriginOverlayImagePath
public string? OriginBackgroundImagePath
{
get;
- init;
+ init
+ {
+ IsVideo = ImageBackgroundManager.IsVideoMediaFileExtensionSupported(value);
+ field = value;
+ }
}
- public string? OriginBackgroundStaticImagePath
+ public string? OriginPlaceholderImagePath
{
get;
init;
}
- public string? OverlayImagePath
+ public bool ForceReload
{
get;
- init;
+ set;
}
- public string? BackgroundImagePath
+ public bool IsVideo
{
get;
- init;
+ private init;
}
- public string? BackgroundImageStaticPath
+ public bool IsCustom
{
get;
init;
}
- public bool ForceReload
+ public async Task GetCachedImagePathAsync(string? path, CancellationToken token)
{
- get;
- init;
- }
+ if (path == null)
+ return null;
- public bool IsVideo
- {
- get;
- init;
+ path = await ImageBackgroundManager.GetLocalOrDownloadedFilePath(path, token).ConfigureAwait(false);
+ (path, _) = await ImageBackgroundManager.GetNativeOrDecodedImagePath(path, token).ConfigureAwait(false);
+
+ if (IsVideo || !ImageBackgroundManager.Shared.GlobalIsWaifu2XEnabled)
+ {
+ return path;
+ }
+
+ Uri upscaledUri = await ImageBackgroundManager.TryGetScaledWaifu2XImagePath(new Uri(path), token).ConfigureAwait(false);
+ return upscaledUri.LocalPath;
}
- public bool IsCustom
+ public void DeleteCachedImagePath(string? path)
{
- get;
- init;
+ if (string.IsNullOrEmpty(path))
+ return;
+
+ Uri uri = new(path);
+ if (ImageBackgroundManager.TryGetDownloadedFile(uri,
+ out FileInfo downloadedFilePath,
+ out _,
+ out _))
+ {
+ downloadedFilePath.TryDeleteFile();
+ }
+
+ if (ImageBackgroundManager.TryGetDecodedTemporaryFile(downloadedFilePath.FullName,
+ out string decodedFilePath))
+ {
+ StreamExtension.TryDeleteFile(decodedFilePath);
+ }
+
+ if (ImageBackgroundManager.TryGetUpscaledFilePath(decodedFilePath,
+ out string upscaledFilePath))
+ {
+ StreamExtension.TryDeleteFile(upscaledFilePath);
+ }
}
public bool Equals(LayeredImageBackgroundContext? other) => other?.GetHashCode() == GetHashCode();
@@ -699,10 +670,7 @@ public override bool Equals(object? obj)
public override int GetHashCode() =>
HashCode.Combine(OriginOverlayImagePath,
OriginBackgroundImagePath,
- OriginBackgroundStaticImagePath,
- OverlayImagePath,
- BackgroundImagePath,
- BackgroundImageStaticPath,
- ForceReload,
- IsVideo);
+ OriginPlaceholderImagePath,
+ IsVideo,
+ IsCustom);
}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/GameManagement/WpfPackage/WpfPackageContext.Methods.cs b/CollapseLauncher/Classes/GameManagement/WpfPackage/WpfPackageContext.Methods.cs
index 9d770bc49e..3743c09ae7 100644
--- a/CollapseLauncher/Classes/GameManagement/WpfPackage/WpfPackageContext.Methods.cs
+++ b/CollapseLauncher/Classes/GameManagement/WpfPackage/WpfPackageContext.Methods.cs
@@ -256,10 +256,7 @@ private async void SpawnUpdateFinishedNotification()
string gameNameTranslated = MetadataHelper.GetTranslatedTitle(gameName);
string gameRegionTranslated = MetadataHelper.GetTranslatedRegion(regionName);
- string icon = await ImageLoaderHelper
- .GetCachedSpritesAsync(WpfPackageIconUrl,
- false,
- CancellationToken.None)
+ string icon = await ImageLoaderHelper.GetCachedSpritesAsync(WpfPackageIconUrl, false, CancellationToken.None)
?? "";
// Create Toast Notification Content
diff --git a/CollapseLauncher/Classes/Helper/Background/ColorPaletteUtility.cs b/CollapseLauncher/Classes/Helper/Background/ColorPaletteUtility.cs
index 6f4c85439c..23d6a92975 100644
--- a/CollapseLauncher/Classes/Helper/Background/ColorPaletteUtility.cs
+++ b/CollapseLauncher/Classes/Helper/Background/ColorPaletteUtility.cs
@@ -62,17 +62,16 @@ private static byte[] GetSharedBufferOrResizeTo(int size)
}
}
- public static async ValueTask GetMediaAccentColorFromAsync(
+ public static ValueTask GetMediaAccentColorFromAsync(
Uri uri,
bool useFfmpegForVideo,
CancellationToken token = default)
{
// -- Ignore if file doesn't exist or not a file URI
string filePath = uri.LocalPath;
- if (!uri.IsFile ||
- !File.Exists(filePath))
+ if (!uri.IsFile || !File.Exists(filePath))
{
- return default;
+ return ValueTask.FromResult(Color.FromArgb(255, 255, 255, 255));
}
// -- Try to get cached palette data
@@ -80,47 +79,63 @@ public static async ValueTask GetMediaAccentColorFromAsync(
out Color cachedColors,
out string cachedPaletteFile))
{
- return cachedColors;
+ return ValueTask.FromResult(cachedColors);
}
- // -- If not cached due to it not existing or has invalid format, generate new palette data
- FileInfo cachedPaletteFileInfo = new FileInfo(cachedPaletteFile)
- .EnsureCreationOfDirectory();
- cachedPaletteFileInfo.Directory?.Create();
-
- // -- Get source stream
- object? sourceStreamObj = await GetMediaFrameStream(filePath, token);
-
- return await Task.Factory.StartNew(Impl, TaskCreationOptions.DenyChildAttach);
-
- Color Impl()
+ TaskCompletionSource processTcs = new();
+ new Thread(Impl)
{
- using IRandomAccessStream? sourceRandomStream = sourceStreamObj as IRandomAccessStream;
- using Stream? sourceStream = sourceStreamObj as Stream ?? sourceRandomStream?.AsStream();
+ IsBackground = true,
+ Name = "Collapse Launcher Media Accent Color Picker"
+ }.UnsafeStart();
+ return new ValueTask(processTcs.Task);
- if (sourceStream == null)
+ async void Impl()
+ {
+ try
{
- return default;
- }
+ // -- If not cached due to it not existing or has invalid format, generate new palette data
+ FileInfo cachedPaletteFileInfo = new FileInfo(cachedPaletteFile).EnsureCreationOfDirectory();
+ cachedPaletteFileInfo.Directory?.Create();
- ImageFileInfo fileInfo = ImageFileInfo.Load(sourceStream);
- ImageFileInfo.FrameInfo frameInfo = fileInfo.Frames[0];
- sourceStream.Seek(0, SeekOrigin.Begin);
+ // -- Get source stream
+ object? sourceStreamObj = await GetMediaFrameStream(filePath, token).ConfigureAwait(false);
- // -- Process palette
- int channels = frameInfo.HasAlpha ? 4 : 3;
- byte[] tempBuffer = GetSharedBufferOrResizeTo(frameInfo.Width * frameInfo.Height * channels + 54);
- using MemoryStream tempStream = new(tempBuffer);
- ProcessImageSettings settings = new();
- settings.TrySetEncoderFormat(ImageMimeTypes.Bmp);
- MagicImageProcessor.ProcessImage(sourceStream, tempStream, settings);
+ using IRandomAccessStream? sourceRandomStream = sourceStreamObj as IRandomAccessStream;
+ await using Stream? sourceStream = sourceStreamObj as Stream ?? sourceRandomStream?.AsStream();
- Span tempDataSpan = tempBuffer.AsSpan(54, (int)tempStream.Position - 54);
- Color colorArray = GetPaletteFromSpan(tempDataSpan, frameInfo.Width, frameInfo.Height, channels);
- ReadOnlySpan colorSpanAsBytes = MemoryMarshal.AsBytes([colorArray]);
+ if (sourceStream == null)
+ {
+ processTcs.SetResult(Color.FromArgb(255, 255, 255, 255));
+ return;
+ }
- File.WriteAllBytes(cachedPaletteFile, colorSpanAsBytes);
- return colorArray;
+ processTcs.SetResult(await Task.Run(() =>
+ {
+ ImageFileInfo fileInfo = ImageFileInfo.Load(sourceStream);
+ ImageFileInfo.FrameInfo frameInfo = fileInfo.Frames[0];
+ sourceStream.Seek(0, SeekOrigin.Begin);
+
+ // -- Process palette
+ int channels = frameInfo.HasAlpha ? 4 : 3;
+ byte[] tempBuffer = GetSharedBufferOrResizeTo(frameInfo.Width * frameInfo.Height * channels + 54);
+ using MemoryStream tempStream = new(tempBuffer);
+ ProcessImageSettings settings = new();
+ settings.TrySetEncoderFormat(ImageMimeTypes.Bmp);
+ MagicImageProcessor.ProcessImage(sourceStream, tempStream, settings);
+
+ Span tempDataSpan = tempBuffer.AsSpan(54, (int)tempStream.Position - 54);
+ Color colorArray = GetPaletteFromSpan(tempDataSpan, frameInfo.Width, frameInfo.Height, channels);
+ ReadOnlySpan colorSpanAsBytes = MemoryMarshal.AsBytes([colorArray]);
+
+ File.WriteAllBytes(cachedPaletteFile, colorSpanAsBytes);
+ return colorArray;
+ }, token));
+ }
+ catch (Exception ex)
+ {
+ processTcs.SetException(ex);
+ }
}
}
diff --git a/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs b/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs
index c839b16361..1c6abf9950 100644
--- a/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs
+++ b/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs
@@ -1,4 +1,5 @@
-using CollapseLauncher.Helper.StreamUtility;
+using CollapseLauncher.GameManagement.ImageBackground;
+using CollapseLauncher.Helper.StreamUtility;
using CollapseLauncher.Plugins;
using CollapseLauncher.XAMLs.Theme.CustomControls;
using Hi3Helper;
@@ -35,21 +36,6 @@ namespace CollapseLauncher.Helper.Image
{
internal static class ImageLoaderHelper
{
- internal static readonly string SupportedImageFormats =
- string.Join(";", LayeredBackgroundImage.SupportedImageBitmapExtensions.Select(x => $"*{x}")) + ";" +
- string.Join(";", LayeredBackgroundImage.SupportedImageBitmapExternalCodecExtensions.Select(x => $"*{x}")) + ";" +
- string.Join(";", LayeredBackgroundImage.SupportedImageVectorExtensions.Select(x => $"*{x}"));
-
- internal static readonly string SupportedVideoFormats =
- string.Join(";", LayeredBackgroundImage.SupportedVideoExtensions.Select(x => $"*{x}"));
-
- internal static readonly Dictionary SupportedBackgroundFormats =
- new() {
- { "All supported formats", SupportedImageFormats + ';' + SupportedVideoFormats },
- { "Image formats", SupportedImageFormats },
- { "Video formats", SupportedVideoFormats }
- };
-
#region Waifu2X
public static Waifu2X Waifu2X;
private static Waifu2XStatus _cachedStatus = Waifu2XStatus.NotInitialized;
@@ -420,8 +406,12 @@ private static async Task GetFallbackStreamUrlInner(HttpClient? client,
public static async Task GetCachedSpritesAsync(HttpClient? httpClient, string? url, bool isSkipHashCheck, CancellationToken token)
{
- if (string.IsNullOrEmpty(url) ||
- ProcessingUrls.Contains(url))
+ if (string.IsNullOrEmpty(url))
+ {
+ return null;
+ }
+
+ if (ProcessingUrls.Contains(url))
{
Logger.LogWriteLine("Found duplicate download request, skipping...\r\n\t" +
$"URL : {url}", LogType.Warning, true);
@@ -432,7 +422,7 @@ private static async Task GetFallbackStreamUrlInner(HttpClient? client,
if (!Directory.Exists(AppGameImgCachedFolder))
Directory.CreateDirectory(AppGameImgCachedFolder);
- FileInfo fInfo = new FileInfo(cachePath);
+ FileInfo fInfo = new(cachePath);
if (await IsFileCompletelyDownloadedAsync(fInfo, !isSkipHashCheck))
{
return cachePath;
@@ -442,6 +432,7 @@ private static async Task GetFallbackStreamUrlInner(HttpClient? client,
{
return url;
}
+
return cachePath;
}
diff --git a/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs b/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs
index cb96a4469a..33df15f4ba 100644
--- a/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs
+++ b/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs
@@ -154,6 +154,9 @@ internal static FileInfo EnsureCreationOfDirectory(this FileInfo filePath)
}
}
+ internal static bool TryDeleteFile(string filePath, bool throwIfFailed = false)
+ => new FileInfo(filePath).TryDeleteFile(throwIfFailed);
+
internal static bool TryDeleteFile(this FileInfo filePath, bool throwIfFailed = false)
{
try
diff --git a/CollapseLauncher/Classes/Helper/StringUtility.cs b/CollapseLauncher/Classes/Helper/StringUtility.cs
new file mode 100644
index 0000000000..39a148afad
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/StringUtility.cs
@@ -0,0 +1,216 @@
+using System;
+using System.Buffers;
+using System.Buffers.Text;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Text;
+#pragma warning disable IDE0130
+
+// ReSharper disable ConvertIfStatementToSwitchStatement
+
+#nullable enable
+namespace CollapseLauncher.Helper;
+
+public static class StringUtility
+{
+ public static bool TryGetStreamFromUrlDataString(
+ ReadOnlySpan originalUriString,
+ out string? mimeType,
+ [NotNullWhen(true)] out MemoryStream? stream)
+ {
+ const string dataScheme = "data:";
+
+ Unsafe.SkipInit(out mimeType);
+ Unsafe.SkipInit(out stream);
+
+ if (originalUriString.IsEmpty)
+ {
+ return false;
+ }
+
+ // -- Check if data is entirely a raw base64 string
+ if (TryGetStreamFromPureBase64String(originalUriString, out stream))
+ {
+ return true;
+ }
+
+ // -- Check if data is entirely a hex string
+ if (TryGetStreamFromPureHexString(originalUriString, out stream))
+ {
+ return true;
+ }
+
+ // -- Check for data scheme
+ int indexOfDataToken = originalUriString.IndexOf(dataScheme);
+ if (indexOfDataToken == -1)
+ {
+ return false;
+ }
+
+ // -- Check for data section
+ int indexOfDataSection = originalUriString.IndexOf(',');
+ if (indexOfDataSection == -1)
+ {
+ return false;
+ }
+
+ // -- Try to get data MIME type
+ ReadOnlySpan mimeToken = originalUriString[dataScheme.Length..indexOfDataSection];
+ if (!mimeToken.IsEmpty)
+ {
+ mimeType = new string(mimeToken);
+ }
+
+ // -- Try to decode the data from base64 string
+ ReadOnlySpan dataSpan = originalUriString[indexOfDataSection..];
+ return TryGetStreamFromPureBase64String(dataSpan, out stream) ||
+ // -- The data is probably a hex string?
+ TryGetStreamFromPureHexString(dataSpan, out stream) ||
+ // -- uhm, probably escaped URL-encoded string? if all these fails, return false.
+ TryGetStreamFromEscapedUrlString(dataSpan, out stream);
+ }
+
+ private static bool TryGetStreamFromPureBase64String(
+ ReadOnlySpan span,
+ [NotNullWhen(true)] out MemoryStream? stream)
+ {
+ Unsafe.SkipInit(out stream);
+
+ int bufferLen = Base64Url.GetMaxDecodedLength(span.Length);
+ byte[]? buffer;
+
+ // -- Allocate test buffer first before allocating real buffer
+ int testBufferLen = Math.Min(1 << 10, span.Length);
+ Span bufferSpan = stackalloc byte[testBufferLen];
+
+ // -- Try to decode the first with test buffer first.
+ // If succeeded, then do hot-path copy and allocate stream.
+ OperationStatus operation = Base64Url.DecodeFromChars(span, bufferSpan, out int offset, out int written);
+ if (operation == OperationStatus.Done)
+ {
+ buffer = GC.AllocateUninitializedArray(written);
+ bufferSpan[..written].CopyTo(buffer); // Copy stack to heap buffer
+
+ stream = new MemoryStream(buffer, 0, written);
+ return true;
+ }
+
+ // -- If failure happened, return false
+ if (operation is OperationStatus.InvalidData or OperationStatus.NeedMoreData)
+ {
+ return false;
+ }
+
+ // -- If more data need to be processed, continue.
+ // But first, copy the previous first test stack buffer to heap buffer.
+ buffer = GC.AllocateUninitializedArray(bufferLen);
+ bufferSpan[..written].CopyTo(buffer);
+
+ // -- Set buffer span and source span, then continue decoding the rest of the data.
+ bufferSpan = buffer.AsSpan(written);
+ span = span[offset..];
+
+ operation = Base64Url.DecodeFromChars(span, bufferSpan, out _, out int writtenNext);
+ if (operation != OperationStatus.Done)
+ {
+ // :/ We wasted some memory due to failure on decoding.
+ // Though, return false.
+ return false;
+ }
+
+ // -- Then, get the memory stream instance.
+ written += writtenNext; // Advance written bytes.
+ stream = new MemoryStream(buffer, 0, written);
+ return true;
+ }
+
+ private static bool TryGetStreamFromEscapedUrlString(
+ ReadOnlySpan span,
+ [NotNullWhen(true)] out MemoryStream? stream)
+ {
+ Unsafe.SkipInit(out stream);
+
+ char[] bufferRent = ArrayPool.Shared.Rent(span.Length);
+ try
+ {
+ if (!Uri.TryUnescapeDataString(span, bufferRent, out int unescapeWritten))
+ {
+ return false;
+ }
+
+ Span bufferRentSpan = bufferRent.AsSpan(0, unescapeWritten);
+
+ int utf8BufferLen = Encoding.UTF8.GetByteCount(bufferRentSpan);
+ byte[] utf8Buffer = GC.AllocateUninitializedArray(utf8BufferLen);
+
+ int utf8Written = Encoding.UTF8.GetBytes(bufferRentSpan, utf8Buffer);
+ stream = new MemoryStream(utf8Buffer, 0, utf8Written);
+ return true;
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(bufferRent);
+ }
+ }
+
+ private static bool TryGetStreamFromPureHexString(
+ ReadOnlySpan span,
+ [NotNullWhen(true)] out MemoryStream? stream)
+ {
+ Unsafe.SkipInit(out stream);
+
+ span = span.Trim(); // Trim possible spaces
+ if (span.Length % 2 != 0) // Not even? definitely invalid.
+ {
+ return false;
+ }
+
+ int bufferLen = span.Length / 2;
+ byte[]? buffer;
+
+ // -- Allocate test buffer first before allocating real buffer
+ int testBufferLen = Math.Min(1 << 10, bufferLen);
+ Span bufferSpan = stackalloc byte[testBufferLen];
+
+ // -- Try to decode the first with test buffer first.
+ // If succeeded, then do hot-path copy and allocate stream.
+ OperationStatus operation = Convert.FromHexString(span, bufferSpan, out int offset, out int written);
+ if (operation == OperationStatus.Done)
+ {
+ buffer = GC.AllocateUninitializedArray(written);
+ bufferSpan[..written].CopyTo(buffer); // Copy stack to heap buffer
+
+ stream = new MemoryStream(buffer, 0, written);
+ return true;
+ }
+
+ // -- If failure happened, return false
+ if (operation is OperationStatus.InvalidData or OperationStatus.NeedMoreData)
+ {
+ return false;
+ }
+
+ // -- If more data need to be processed, continue.
+ // But first, copy the previous first test stack buffer to heap buffer.
+ buffer = GC.AllocateUninitializedArray(bufferLen);
+ bufferSpan[..written].CopyTo(buffer);
+
+ // -- Set buffer span and source span, then continue decoding the rest of the data.
+ bufferSpan = buffer.AsSpan(written);
+ span = span[offset..];
+
+ operation = Convert.FromHexString(span, bufferSpan, out _, out int writtenNext);
+ if (operation != OperationStatus.Done)
+ {
+ // :/ We wasted some memory due to failure on decoding.
+ // Though, return false.
+ return false;
+ }
+
+ // -- Then, get the memory stream instance.
+ written += writtenNext; // Advance written bytes.
+ stream = new MemoryStream(buffer, 0, written);
+ return true;
+ }
+}
diff --git a/CollapseLauncher/Classes/Helper/WindowUtility.cs b/CollapseLauncher/Classes/Helper/WindowUtility.cs
index af1981e3ed..72c72197c6 100644
--- a/CollapseLauncher/Classes/Helper/WindowUtility.cs
+++ b/CollapseLauncher/Classes/Helper/WindowUtility.cs
@@ -156,7 +156,7 @@ internal static bool CurrentWindowIsVisible
get
{
WS_STYLE windowStyle = PInvoke.GetWindowLong(CurrentWindowPtr, GWL_INDEX.GWL_STYLE);
- bool isHide = windowStyle.HasFlag(WS_STYLE.WS_MINIMIZE);
+ bool isHide = windowStyle.HasFlag(WS_STYLE.WS_MINIMIZE) || !windowStyle.HasFlag(WS_STYLE.WS_VISIBLE);
return !isHide;
}
}
diff --git a/CollapseLauncher/Classes/RegionManagement/RegionManagement.cs b/CollapseLauncher/Classes/RegionManagement/RegionManagement.cs
index a33361e46f..592ab1d5e9 100644
--- a/CollapseLauncher/Classes/RegionManagement/RegionManagement.cs
+++ b/CollapseLauncher/Classes/RegionManagement/RegionManagement.cs
@@ -168,14 +168,6 @@ Task BeforeLoadRoutine(CancellationToken token)
LogWriteLine($"Initializing game: {regionToChangeName}...", LogType.Scheme, true);
ClearMainPageState();
-
- bool isCustom = LauncherConfig.GetAppConfigValue($"LastIsCustomImageEnabled-{preset.GameName}-{preset.ZoneName}").ToBool();
- if (!isCustom)
- {
- DispatcherQueue.TryEnqueue(() =>
- ImageBackgroundManager.Shared.PreloadBackground(preset, BackgroundPresenterGrid));
- }
-
_ = ShowAsyncLoadingTimedOutPill(token);
}
catch (Exception ex)
diff --git a/CollapseLauncher/CollapseLauncher.csproj b/CollapseLauncher/CollapseLauncher.csproj
index e71960ace3..e3d488246b 100644
--- a/CollapseLauncher/CollapseLauncher.csproj
+++ b/CollapseLauncher/CollapseLauncher.csproj
@@ -321,6 +321,7 @@
+
diff --git a/CollapseLauncher/Program.cs b/CollapseLauncher/Program.cs
index 998d8f1422..5215f28577 100644
--- a/CollapseLauncher/Program.cs
+++ b/CollapseLauncher/Program.cs
@@ -9,6 +9,7 @@
using Hi3Helper.EncTool;
using Hi3Helper.EncTool.Hashes;
using Hi3Helper.Http.Legacy;
+using Hi3Helper.MagicScaler.LibheifCL;
using Hi3Helper.SentryHelper;
using Hi3Helper.Shared.ClassStruct;
using Hi3Helper.Win32.ManagedTools;
@@ -19,13 +20,15 @@
using Microsoft.UI.Xaml.Settings;
using Microsoft.Win32;
using PhotoSauce.MagicScaler;
-using PhotoSauce.NativeCodecs.Libheif;
using PhotoSauce.NativeCodecs.Libjxl;
using PhotoSauce.NativeCodecs.Libwebp;
using System;
+using System.Buffers;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
+using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -326,6 +329,11 @@ private static void InitCriticalModules()
ZstdNet.DllUtils.IsIgnoreMissingLibrary = true;
#endif
+ /* ---------------------------------------------------------------------------------------------
+ * Module: MagicScaler External Codecs for Image Decoding
+ */
+ InitMagicScalerExternalCodecs();
+
/* ---------------------------------------------------------------------------------------------
* Module: Velopack
*/
@@ -369,11 +377,6 @@ private static async void InitOtherSdkAsync()
*/
await InitDatabaseHandler();
- /* ---------------------------------------------------------------------------------------------
- * Module: MagicScaler External Codecs for Image Decoding
- */
- InitMagicScalerExternalCodecs();
-
/* ---------------------------------------------------------------------------------------------
* Module: Waifu2X (Start device test in the background and cache it)
*/
@@ -389,12 +392,7 @@ private static void InitMagicScalerExternalCodecs()
{
try
{
- CodecManager.Configure(codecs =>
- {
- codecs.UseLibwebp();
- codecs.UseLibheif();
- codecs.UseLibjxl();
- });
+ CodecManager.Configure(RegisterExternalCodec);
}
catch (Exception ex)
{
@@ -402,6 +400,29 @@ private static void InitMagicScalerExternalCodecs()
LogType.Error,
true);
}
+
+ return;
+
+ static void RegisterExternalCodec(CodecCollection codecs)
+ {
+ // Remove built-in WIC decoder from the list before adding another ones.
+ SearchValues searchValues = SearchValues.Create([
+ "Microsoft HEIF",
+ "Microsoft Webp",
+ "Microsoft JPEG-XL",
+ "Microsoft JXL"
+ ], StringComparison.OrdinalIgnoreCase);
+
+ List toDeleteList = [.. codecs.Where(x => x.Name.ContainsAny(searchValues))];
+ foreach (IImageCodecInfo toDelete in toDeleteList)
+ {
+ codecs.Remove(toDelete);
+ }
+
+ codecs.UseLibwebp();
+ codecs.UseLibheifCL();
+ codecs.UseLibjxl();
+ }
}
private static void InitSentrySdk()
diff --git a/CollapseLauncher/Properties/launchSettings.json b/CollapseLauncher/Properties/launchSettings.json
index eb3b60bd14..1afb9fd779 100644
--- a/CollapseLauncher/Properties/launchSettings.json
+++ b/CollapseLauncher/Properties/launchSettings.json
@@ -2,8 +2,8 @@
"profiles": {
"Collapse Launcher": {
"commandName": "Project",
- "nativeDebugging": false,
- "hotReloadEnabled": true
+ "hotReloadEnabled": true,
+ "debugEngines": "managed-dotnet"
},
"OOBE Setup": {
"commandName": "Project",
diff --git a/CollapseLauncher/XAMLs/MainApp/MainPage.Notification.cs b/CollapseLauncher/XAMLs/MainApp/MainPage.Notification.cs
index 14692283af..aa0b65d693 100644
--- a/CollapseLauncher/XAMLs/MainApp/MainPage.Notification.cs
+++ b/CollapseLauncher/XAMLs/MainApp/MainPage.Notification.cs
@@ -1,4 +1,5 @@
using CollapseLauncher.Extension;
+using CollapseLauncher.GameManagement.ImageBackground;
using CollapseLauncher.Helper;
using CollapseLauncher.Helper.StreamUtility;
using CollapseLauncher.Helper.Update;
diff --git a/CollapseLauncher/XAMLs/MainApp/MainPage.xaml b/CollapseLauncher/XAMLs/MainApp/MainPage.xaml
index 12d2dcafd4..fe14e7d822 100644
--- a/CollapseLauncher/XAMLs/MainApp/MainPage.xaml
+++ b/CollapseLauncher/XAMLs/MainApp/MainPage.xaml
@@ -27,14 +27,14 @@
+
+ BackgroundSource="{x:Bind PlaceholderBackgroundImage, Converter={StaticResource ImageDecoderValueConverter}}">
@@ -444,7 +444,7 @@
Margin="-2,-2,0,-2"
VerticalAlignment="Stretch"
Opacity="0.8"
- Source="{ThemeResource AppLogo}">
+ Source="ms-appx:///Assets/CollapseLauncherLogo.png">
diff --git a/CollapseLauncher/XAMLs/MainApp/MainPage.xaml.cs b/CollapseLauncher/XAMLs/MainApp/MainPage.xaml.cs
index be8ba59619..15e7f7258c 100644
--- a/CollapseLauncher/XAMLs/MainApp/MainPage.xaml.cs
+++ b/CollapseLauncher/XAMLs/MainApp/MainPage.xaml.cs
@@ -72,7 +72,6 @@ private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
private int _currentGameRegion = -1;
internal Uri PlaceholderBackgroundImage => new(ImageBackgroundManager.GetPlaceholderBackgroundImageFrom(CurrentGameProperty?.GamePreset));
- internal string PlaceholderDecodedCacheDir => AppGameImgFolder;
internal ImageBackgroundManager CurrentBackgroundManager => ImageBackgroundManager.Shared;
#endregion
diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.Background.cs b/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.Background.cs
index e238a62fd6..b152aeed21 100644
--- a/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.Background.cs
+++ b/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.Background.cs
@@ -30,7 +30,7 @@
using System.Threading.Tasks;
using System.Windows.Input;
using Windows.Foundation;
-
+using Windows.Media.Playback;
// ReSharper disable CheckNamespace
#nullable enable
@@ -42,7 +42,7 @@ public HypLauncherBackgroundList? CurrentGameBackgroundData
{
get
{
- ILauncherApi? api = CurrentPresetConfig?.GameLauncherApi;
+ ILauncherApi? api = CurrentPresetConfig == null! ? null : CurrentPresetConfig.GameLauncherApi;
HypLauncherBackgroundList? data = api?.LauncherGameBackground?.Data;
return data;
}
@@ -173,7 +173,7 @@ private static void ToggleMultiBackgroundPipsPagerGridHoverState(Panel grid, boo
grid.Opacity = 1f; // Trigger this bihhh to appear on the visual first since its own .Opacity and Visual.Opacity don't match.
visual.Opacity = show ? 0f : 1f;
- double duration = 200d;
+ const double duration = 200d;
CompositionAnimationGroup animGroup = compositor.CreateAnimationGroup();
CompositionEasingFunction? function = compositor.TryCreateEasingFunction(EasingType.Quintic);
@@ -228,6 +228,7 @@ private static void ToggleMultiBackgroundPipsPagerGridHoverState(Panel grid, boo
grid.IsHitTestVisible = false;
}
}
+
#endregion
#region Background context menu commands
@@ -235,32 +236,18 @@ private static void ToggleMultiBackgroundPipsPagerGridHoverState(Panel grid, boo
public ICommand BackgroundImageContextMenuCopyUrlToClipboardCommand { get; } = new RelayCommand