diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs index 6f874831..0f40c101 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs @@ -69,12 +69,12 @@ public sealed partial class App : Application if (firstInstance.IsCurrent) { logger.LogInformation(ConsoleBanner); + LogDiagnosticInformation(); // manually invoke activation.NonRedirectToActivate(firstInstance, activatedEventArgs); activation.InitializeWith(firstInstance); - LogDiagnosticInformation(); serviceProvider.GetRequiredService().ConfigureAsync().SafeForget(); } else diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs index ef166a7e..fa1a7c77 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.cs @@ -4,6 +4,8 @@ using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; using Snap.Hutao.Core.Caching; +using Snap.Hutao.Model.Metadata.Converter; +using Snap.Hutao.Web; using System.Runtime.InteropServices; namespace Snap.Hutao.Control.Image; @@ -40,12 +42,7 @@ internal sealed class CachedImage : Implementation.ImageEx { // The image is corrupted, remove it. imageCache.Remove(imageUri); - return null; - } - catch (OperationCanceledException) - { - // task was explicitly canceled - return null; + return default; } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.xaml index b9540edd..f74b45dd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.xaml @@ -17,6 +17,7 @@ CornerRadius="{TemplateBinding CornerRadius}"> (int)GetValue(DecodePixelHeightProperty); - set => SetValue(DecodePixelHeightProperty, value); - } - - public DecodePixelType DecodePixelType - { - get => (DecodePixelType)GetValue(DecodePixelTypeProperty); - set => SetValue(DecodePixelTypeProperty, value); - } - - public int DecodePixelWidth - { - get => (int)GetValue(DecodePixelWidthProperty); - set => SetValue(DecodePixelWidthProperty, value); - } - - public Stretch Stretch - { - get => (Stretch)GetValue(StretchProperty); - set => SetValue(StretchProperty, value); - } - - public bool IsCacheEnabled - { - get => (bool)GetValue(IsCacheEnabledProperty); - set => SetValue(IsCacheEnabledProperty, value); - } - - public bool EnableLazyLoading - { - get => (bool)GetValue(EnableLazyLoadingProperty); - set => SetValue(EnableLazyLoadingProperty, value); - } - - public double LazyLoadingThreshold - { - get => (double)GetValue(LazyLoadingThresholdProperty); - set => SetValue(LazyLoadingThresholdProperty, value); - } - - public ImageSource PlaceholderSource - { - get => (ImageSource)GetValue(PlaceholderSourceProperty); - set => SetValue(PlaceholderSourceProperty, value); - } - - public Stretch PlaceholderStretch - { - get => (Stretch)GetValue(PlaceholderStretchProperty); - set => SetValue(PlaceholderStretchProperty, value); - } - - public object Source - { - get => GetValue(SourceProperty); - set => SetValue(SourceProperty, value); - } - public bool WaitUntilLoaded { get => true; @@ -121,11 +53,9 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha protected object? Image { get; private set; } - public abstract CompositionBrush GetAlphaMask(); + protected object? PlaceholderImage { get; private set; } - protected virtual void OnPlaceholderSourceChanged(DependencyPropertyChangedEventArgs e) - { - } + public abstract CompositionBrush GetAlphaMask(); protected virtual Task ProvideCachedResourceAsync(Uri imageUri, CancellationToken token) { @@ -136,61 +66,11 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha protected virtual void OnImageOpened(object sender, RoutedEventArgs e) { VisualStateManager.GoToState(this, LoadedState, true); - ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs()); } protected virtual void OnImageFailed(object sender, ExceptionRoutedEventArgs e) { VisualStateManager.GoToState(this, FailedState, true); - ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new FileNotFoundException(e.ErrorMessage))); - } - - protected void AttachImageOpened(RoutedEventHandler handler) - { - if (Image is Microsoft.UI.Xaml.Controls.Image image) - { - image.ImageOpened += handler; - } - else if (Image is ImageBrush brush) - { - brush.ImageOpened += handler; - } - } - - protected void RemoveImageOpened(RoutedEventHandler handler) - { - if (Image is Microsoft.UI.Xaml.Controls.Image image) - { - image.ImageOpened -= handler; - } - else if (Image is ImageBrush brush) - { - brush.ImageOpened -= handler; - } - } - - protected void AttachImageFailed(ExceptionRoutedEventHandler handler) - { - if (Image is Microsoft.UI.Xaml.Controls.Image image) - { - image.ImageFailed += handler; - } - else if (Image is ImageBrush brush) - { - brush.ImageFailed += handler; - } - } - - protected void RemoveImageFailed(ExceptionRoutedEventHandler handler) - { - if (Image is Microsoft.UI.Xaml.Controls.Image image) - { - image.ImageFailed -= handler; - } - else if (Image is ImageBrush brush) - { - brush.ImageFailed -= handler; - } } protected override void OnApplyTemplate() @@ -199,11 +79,10 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha RemoveImageFailed(OnImageFailed); Image = GetTemplateChild(PartImage); + PlaceholderImage = GetTemplateChild(PartPlaceholderImage); IsInitialized = true; - ImageExInitialized?.Invoke(this, EventArgs.Empty); - if (Source is null || !EnableLazyLoading || isInViewport) { lazyLoadingSource = null; @@ -218,23 +97,73 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha AttachImageFailed(OnImageFailed); base.OnApplyTemplate(); + + void AttachImageOpened(RoutedEventHandler handler) + { + if (Image is Microsoft.UI.Xaml.Controls.Image image) + { + image.ImageOpened += handler; + } + else if (Image is ImageBrush brush) + { + brush.ImageOpened += handler; + } + } + + void AttachImageFailed(ExceptionRoutedEventHandler handler) + { + if (Image is Microsoft.UI.Xaml.Controls.Image image) + { + image.ImageFailed += handler; + } + else if (Image is ImageBrush brush) + { + brush.ImageFailed += handler; + } + } + + void RemoveImageOpened(RoutedEventHandler handler) + { + if (Image is Microsoft.UI.Xaml.Controls.Image image) + { + image.ImageOpened -= handler; + } + else if (Image is ImageBrush brush) + { + brush.ImageOpened -= handler; + } + } + + void RemoveImageFailed(ExceptionRoutedEventHandler handler) + { + if (Image is Microsoft.UI.Xaml.Controls.Image image) + { + image.ImageFailed -= handler; + } + else if (Image is ImageBrush brush) + { + brush.ImageFailed -= handler; + } + } } private static void EnableLazyLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - if (d is ImageExBase control) + if (d is not ImageExBase control) { - bool value = (bool)e.NewValue; - if (value) - { - control.LayoutUpdated += control.ImageExBase_LayoutUpdated; + return; + } - control.InvalidateLazyLoading(); - } - else - { - control.LayoutUpdated -= control.ImageExBase_LayoutUpdated; - } + bool value = (bool)e.NewValue; + if (value) + { + control.LayoutUpdated += control.OnImageExBaseLayoutUpdated; + + control.InvalidateLazyLoading(); + } + else + { + control.LayoutUpdated -= control.OnImageExBaseLayoutUpdated; } } @@ -246,14 +175,6 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha } } - private static void PlaceholderSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is ImageExBase control) - { - control.OnPlaceholderSourceChanged(e); - } - } - private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not ImageExBase control) @@ -261,17 +182,19 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha return; } - if (e.OldValue is null || e.NewValue is null || !e.OldValue.Equals(e.NewValue)) + if (e.OldValue is not null && e.NewValue is not null && e.OldValue.Equals(e.NewValue)) { - if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport) - { - control.lazyLoadingSource = null; - control.SetSource(e.NewValue); - } - else - { - control.lazyLoadingSource = e.NewValue; - } + return; + } + + if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport) + { + control.lazyLoadingSource = null; + control.SetSource(e.NewValue); + } + else + { + control.lazyLoadingSource = e.NewValue; } } @@ -301,11 +224,24 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 }) { VisualStateManager.GoToState(this, LoadedState, true); - ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs()); } } - [SuppressMessage("", "IDE0019")] + private void AttachPlaceholderSource(ImageSource? source) + { + // Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState + // as we register to both the ImageOpened/ImageFailed events of the underlying control. + // We only need to call those methods if we fail in other cases before we get here. + if (PlaceholderImage is Microsoft.UI.Xaml.Controls.Image image) + { + image.Source = source; + } + else if (PlaceholderImage is ImageBrush brush) + { + brush.ImageSource = source; + } + } + private async void SetSource(object? source) { if (!IsInitialized) @@ -326,22 +262,19 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha VisualStateManager.GoToState(this, LoadingState, true); - ImageSource? imageSource = source as ImageSource; - if (imageSource is not null) + if (source as ImageSource is { } imageSource) { AttachSource(imageSource); return; } - Uri? uri = source as Uri; - if (uri is null) + if (source as Uri is not { } uri) { string? url = source as string ?? source.ToString(); if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri)) { VisualStateManager.GoToState(this, FailedState, true); - ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new UriFormatException("Invalid uri specified."))); return; } } @@ -355,61 +288,131 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha { await LoadImageAsync(uri, tokenSource.Token).ConfigureAwait(true); } + catch (Exception ex) + { + SetPlaceholderSource(PlaceholderSource); + + if (ex is OperationCanceledException) + { + // nothing to do as cancellation has been requested. + } + else + { + VisualStateManager.GoToState(this, FailedState, true); + } + } + } + + private async void SetPlaceholderSource(object? source) + { + if (!IsInitialized) + { + return; + } + + tokenSource?.Cancel(); + + tokenSource = new CancellationTokenSource(); + + AttachPlaceholderSource(null); + + if (source is null) + { + return; + } + + if (source as ImageSource is { } imageSource) + { + AttachPlaceholderSource(imageSource); + + return; + } + + if (source as Uri is not { } uri) + { + string? url = source as string ?? source.ToString(); + if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri)) + { + return; + } + } + + if (!IsHttpUri(uri) && !uri.IsAbsoluteUri) + { + uri = new Uri("ms-appx:///" + uri.OriginalString.TrimStart('/')); + } + + try + { + if (uri is null) + { + return; + } + + ImageSource? img = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true); + + ArgumentNullException.ThrowIfNull(tokenSource); + if (!tokenSource.IsCancellationRequested) + { + // Only attach our image if we still have a valid request. + AttachPlaceholderSource(img); + } + } catch (OperationCanceledException) { // nothing to do as cancellation has been requested. } - catch (Exception e) + catch { - VisualStateManager.GoToState(this, FailedState, true); - ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e)); } } private async Task LoadImageAsync(Uri imageUri, CancellationToken token) { - if (imageUri is not null) + if (imageUri is null) { - if (IsCacheEnabled) + return; + } + + if (IsCacheEnabled) + { + ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true); + + ArgumentNullException.ThrowIfNull(tokenSource); + if (!tokenSource.IsCancellationRequested) { - ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true); + // Only attach our image if we still have a valid request. + AttachSource(img); + } + } + else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase)) + { + string source = imageUri.OriginalString; + const string base64Head = "base64,"; + int index = source.IndexOf(base64Head, StringComparison.Ordinal); + if (index >= 0) + { + byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]); + BitmapImage bitmap = new(); + await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream()); ArgumentNullException.ThrowIfNull(tokenSource); if (!tokenSource.IsCancellationRequested) { - // Only attach our image if we still have a valid request. - AttachSource(img); + AttachSource(bitmap); } } - else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase)) + } + else + { + AttachSource(new BitmapImage(imageUri) { - string source = imageUri.OriginalString; - const string base64Head = "base64,"; - int index = source.IndexOf(base64Head, StringComparison.Ordinal); - if (index >= 0) - { - byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]); - BitmapImage bitmap = new(); - await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream()); - - ArgumentNullException.ThrowIfNull(tokenSource); - if (!tokenSource.IsCancellationRequested) - { - AttachSource(bitmap); - } - } - } - else - { - AttachSource(new BitmapImage(imageUri) - { - CreateOptions = BitmapCreateOptions.IgnoreImageCache, - }); - } + CreateOptions = BitmapCreateOptions.IgnoreImageCache, + }); } } - private void ImageExBase_LayoutUpdated(object? sender, object e) + private void OnImageExBaseLayoutUpdated(object? sender, object e) { InvalidateLazyLoading(); } diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/Implementation/ImageExFailedEventArgs.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/Implementation/ImageExFailedEventArgs.cs deleted file mode 100644 index b6841b6c..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/Implementation/ImageExFailedEventArgs.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Control.Image.Implementation; - -internal sealed class ImageExFailedEventArgs : EventArgs -{ - public ImageExFailedEventArgs(Exception errorException) - { - ErrorMessage = ErrorException?.Message; - ErrorException = errorException; - } - - public Exception? ErrorException { get; private set; } - - public string? ErrorMessage { get; private set; } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Image/Implementation/ImageExOpenedEventArgs.cs b/src/Snap.Hutao/Snap.Hutao/Control/Image/Implementation/ImageExOpenedEventArgs.cs deleted file mode 100644 index b6926fed..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Control/Image/Implementation/ImageExOpenedEventArgs.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Control.Image.Implementation; - -internal sealed class ImageExOpenedEventArgs : EventArgs -{ -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Uri.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Uri.xaml index 5ee6d30c..1d1a7387 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Uri.xaml +++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Uri.xaml @@ -29,6 +29,7 @@ https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png + https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon271.png https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon445.png diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs index 32be4c21..436613ad 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs @@ -12,6 +12,7 @@ using Snap.Hutao.Web.Response; using System.Collections.Specialized; using System.IO; using System.Web; +using Windows.Foundation; namespace Snap.Hutao.View.Dialog; diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml index 2d21e47d..234ad611 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml @@ -42,7 +42,12 @@ VerticalAlignment="Top" cw:VisualExtensions.NormalizedCenterPoint="0.5"> - + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index 372c0b72..1cf81a16 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -7,8 +7,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mxi="using:Microsoft.Xaml.Interactivity" - xmlns:mxic="using:Microsoft.Xaml.Interactions.Core" - xmlns:mxim="using:Microsoft.Xaml.Interactions.Media" xmlns:shc="using:Snap.Hutao.Control" xmlns:shcb="using:Snap.Hutao.Control.Behavior" xmlns:shccs="using:Snap.Hutao.Control.Collection.Selector" @@ -208,8 +206,7 @@ shch:SettingsExpanderHelper.IsItemsEnabled="{Binding LaunchOptions.IsEnabled}" Description="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsDescription}" Header="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsHeader}" - HeaderIcon="{shcm:FontIcon Glyph=}" - IsExpanded="True"> + HeaderIcon="{shcm:FontIcon Glyph=}"> diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpContentSerializationException.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpContentSerializationException.cs index 2dd2a915..8da2aabc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpContentSerializationException.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpContentSerializationException.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using System.Net.Http; + namespace Snap.Hutao.Web.Request.Builder; [Serializable] @@ -11,6 +13,31 @@ internal sealed class HttpContentSerializationException : Exception { } + private HttpContentSerializationException(Exception? innerException) + : base(GetDefaultMessage(), innerException) + { + } + + public static async ValueTask CreateAsync(HttpContent? content, Exception? innerException) + { + if (content is null) + { + return new(innerException); + } + + string contentString = await content.ReadAsStringAsync().ConfigureAwait(false); + string message = $""" + The (de-)serialization failed because of an arbitrary error. This most likely happened, + because an inner serializer failed to (de-)serialize the given data. + ----- data begin ----- + {contentString} + ----- data end ----- + See the inner exception for details (if available). + """; + + return new(message, innerException); + } + private static string GetDefaultMessage() { return """ diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpContentSerializer.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpContentSerializer.cs index ee1487cc..5ce9040a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpContentSerializer.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpContentSerializer.cs @@ -49,7 +49,7 @@ internal abstract class HttpContentSerializer : IHttpContentSerializer, IHttpCon } catch (Exception ex) when (ex is not HttpContentSerializationException) { - throw new HttpContentSerializationException(null, ex); + throw await HttpContentSerializationException.CreateAsync(httpContent, ex).ConfigureAwait(false); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpTryCatchSendStrategy.cs b/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpTryCatchSendStrategy.cs deleted file mode 100644 index 53eb0ea1..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/HttpTryCatchSendStrategy.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Request.Builder; - -internal enum HttpTryCatchSendStrategy -{ - Default, - HutaoApi, -} \ No newline at end of file