mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
refactor controls
This commit is contained in:
@@ -14,17 +14,24 @@ namespace Snap.Hutao.Control.Behavior;
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AutoHeightBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
|
||||
public AutoHeightBehavior()
|
||||
{
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAssociatedObjectLoaded()
|
||||
{
|
||||
UpdateElement();
|
||||
AssociatedObject.SizeChanged += OnSizeChanged;
|
||||
AssociatedObject.SizeChanged += sizeChangedEventHandler;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
AssociatedObject.SizeChanged -= OnSizeChanged;
|
||||
AssociatedObject.SizeChanged -= sizeChangedEventHandler;
|
||||
base.OnDetaching();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,17 +14,24 @@ namespace Snap.Hutao.Control.Behavior;
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AutoWidthBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
|
||||
public AutoWidthBehavior()
|
||||
{
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAssociatedObjectLoaded()
|
||||
{
|
||||
UpdateElement();
|
||||
AssociatedObject.SizeChanged += OnSizeChanged;
|
||||
AssociatedObject.SizeChanged += sizeChangedEventHandler;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
AssociatedObject.SizeChanged -= OnSizeChanged;
|
||||
AssociatedObject.SizeChanged -= sizeChangedEventHandler;
|
||||
base.OnDetaching();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace Snap.Hutao.Control.Behavior;
|
||||
internal sealed class ComboBoxExtendsContentIntoTitleBarWorkaroundBehavior : BehaviorBase<ComboBox>
|
||||
{
|
||||
private readonly IMessenger messenger;
|
||||
private readonly EventHandler<object> dropDownOpenedHandler;
|
||||
private readonly EventHandler<object> dropDownClosedHandler;
|
||||
|
||||
/// <summary>
|
||||
/// AppTitleBar Workaround
|
||||
@@ -21,31 +23,33 @@ internal sealed class ComboBoxExtendsContentIntoTitleBarWorkaroundBehavior : Beh
|
||||
public ComboBoxExtendsContentIntoTitleBarWorkaroundBehavior()
|
||||
{
|
||||
messenger = Ioc.Default.GetRequiredService<IMessenger>();
|
||||
dropDownOpenedHandler = OnDropDownOpened;
|
||||
dropDownClosedHandler = OnDropDownClosed;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAssociatedObjectLoaded()
|
||||
{
|
||||
AssociatedObject.DropDownOpened += OnDropDownOpened;
|
||||
AssociatedObject.DropDownClosed += OnDropDownClosed;
|
||||
AssociatedObject.DropDownOpened += dropDownOpenedHandler;
|
||||
AssociatedObject.DropDownClosed += dropDownClosedHandler;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
AssociatedObject.DropDownOpened -= OnDropDownOpened;
|
||||
AssociatedObject.DropDownClosed -= OnDropDownClosed;
|
||||
AssociatedObject.DropDownOpened -= dropDownOpenedHandler;
|
||||
AssociatedObject.DropDownClosed -= dropDownClosedHandler;
|
||||
|
||||
base.OnDetaching();
|
||||
}
|
||||
|
||||
private void OnDropDownOpened(object? sender, object e)
|
||||
{
|
||||
messenger.Send(new Message.FlyoutOpenCloseMessage(true));
|
||||
messenger.Send(Message.FlyoutStateChangedMessage.Open);
|
||||
}
|
||||
|
||||
private void OnDropDownClosed(object? sender, object e)
|
||||
{
|
||||
messenger.Send(new Message.FlyoutOpenCloseMessage(false));
|
||||
messenger.Send(Message.FlyoutStateChangedMessage.Close);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,6 @@ internal sealed class OpenAttachedFlyoutAction : DependencyObject, IAction
|
||||
public object Execute(object sender, object parameter)
|
||||
{
|
||||
FlyoutBase.ShowAttachedFlyout(sender as FrameworkElement);
|
||||
return null!;
|
||||
return default!;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ internal static class BoxedValues
|
||||
public static readonly object DoubleZero = 0D;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="double"/> 0
|
||||
/// <see cref="double"/> 1
|
||||
/// </summary>
|
||||
public static readonly object DoubleOne = 1D;
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ internal static class ContentDialogExtension
|
||||
public static async ValueTask<ContentDialogHideToken> BlockAsync(this ContentDialog contentDialog, ITaskContext taskContext)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
contentDialog.ShowAsync().AsTask().SafeForget();
|
||||
|
||||
// E_ASYNC_OPERATION_NOT_STARTED 0x80000019
|
||||
// Only a single ContentDialog can be open at any time.
|
||||
contentDialog.ShowAsync().AsTask().SafeForget();
|
||||
return new ContentDialogHideToken(contentDialog, taskContext);
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,14 @@ using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Extension;
|
||||
|
||||
internal readonly struct ContentDialogHideToken : IDisposable
|
||||
internal struct ContentDialogHideToken : IDisposable, IAsyncDisposable
|
||||
{
|
||||
private readonly ContentDialog contentDialog;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
private bool disposed = false;
|
||||
private bool disposing = false;
|
||||
|
||||
public ContentDialogHideToken(ContentDialog contentDialog, ITaskContext taskContext)
|
||||
{
|
||||
this.contentDialog = contentDialog;
|
||||
@@ -18,7 +21,24 @@ internal readonly struct ContentDialogHideToken : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Hide() must be called on main thread.
|
||||
taskContext.InvokeOnMainThread(contentDialog.Hide);
|
||||
if (!disposed && !disposing)
|
||||
{
|
||||
disposing = true;
|
||||
taskContext.InvokeOnMainThread(contentDialog.Hide); // Hide() must be called on main thread.
|
||||
disposing = false;
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!disposed && !disposing)
|
||||
{
|
||||
disposing = true;
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
contentDialog.Hide();
|
||||
disposing = false;
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Extension;
|
||||
|
||||
internal static class DependencyObjectExtension
|
||||
{
|
||||
public static IServiceProvider ServiceProvider(this DependencyObject obj)
|
||||
{
|
||||
return Ioc.Default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Extension;
|
||||
|
||||
internal static class FrameworkElementExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Make properties below false:
|
||||
/// <code>
|
||||
/// * AllowFocusOnInteraction
|
||||
/// * IsDoubleTapEnabled
|
||||
/// * IsHitTestVisible
|
||||
/// * IsHoldingEnabled
|
||||
/// * IsRightTapEnabled
|
||||
/// * IsTabStop
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="frameworkElement">元素</param>
|
||||
public static void DisableInteraction(this FrameworkElement frameworkElement)
|
||||
{
|
||||
frameworkElement.AllowFocusOnInteraction = false;
|
||||
frameworkElement.IsDoubleTapEnabled = false;
|
||||
frameworkElement.IsHitTestVisible = false;
|
||||
frameworkElement.IsHoldingEnabled = false;
|
||||
frameworkElement.IsRightTapEnabled = false;
|
||||
frameworkElement.IsTabStop = false;
|
||||
}
|
||||
}
|
||||
@@ -32,16 +32,10 @@ internal sealed class CachedImage : ImageEx
|
||||
|
||||
try
|
||||
{
|
||||
Verify.Operation(imageUri.Host != string.Empty, SH.ControlImageCachedImageInvalidResourceUri);
|
||||
|
||||
// BitmapImage need to be created by main thread.
|
||||
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true);
|
||||
|
||||
// check token state to determine whether the operation should be canceled.
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// BitmapImage initialize with a uri will increase image quality and loading speed.
|
||||
return new BitmapImage(file.ToUri());
|
||||
Verify.Operation(!string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
|
||||
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
|
||||
token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled.
|
||||
return new BitmapImage(file.ToUri()); // BitmapImage initialize with a uri will increase image quality and loading speed.
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
|
||||
@@ -7,11 +7,13 @@ using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Control.Animation;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Image;
|
||||
|
||||
@@ -28,6 +30,11 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
private readonly RoutedEventHandler unloadEventHandler;
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
private readonly TypedEventHandler<LoadedImageSurface, LoadedImageSourceLoadCompletedEventArgs> loadedImageSourceLoadCompletedEventHandler;
|
||||
|
||||
private TaskCompletionSource? surfaceLoadTaskCompletionSource;
|
||||
private SpriteVisual? spriteVisual;
|
||||
private bool isShow = true;
|
||||
|
||||
@@ -36,16 +43,16 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
/// </summary>
|
||||
protected CompositionImage()
|
||||
{
|
||||
serviceProvider = Ioc.Default;
|
||||
serviceProvider = this.ServiceProvider();
|
||||
this.DisableInteraction();
|
||||
|
||||
AllowFocusOnInteraction = false;
|
||||
IsDoubleTapEnabled = false;
|
||||
IsHitTestVisible = false;
|
||||
IsHoldingEnabled = false;
|
||||
IsRightTapEnabled = false;
|
||||
IsTabStop = false;
|
||||
unloadEventHandler = OnUnload;
|
||||
Unloaded += unloadEventHandler;
|
||||
|
||||
SizeChanged += OnSizeChanged;
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
SizeChanged += sizeChangedEventHandler;
|
||||
|
||||
loadedImageSourceLoadCompletedEventHandler = OnLoadImageSurfaceLoadCompleted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,26 +72,19 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
/// <returns>拼合视觉</returns>
|
||||
protected abstract SpriteVisual CompositeSpriteVisual(Compositor compositor, LoadedImageSurface imageSurface);
|
||||
|
||||
/// <summary>
|
||||
/// 异步加载图像表面
|
||||
/// </summary>
|
||||
/// <param name="file">文件</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>加载的图像表面</returns>
|
||||
protected virtual async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
|
||||
protected virtual void LoadImageSurfaceCompleted(LoadedImageSurface surface)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void Unloading()
|
||||
{
|
||||
TaskCompletionSource loadCompleteTaskSource = new();
|
||||
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(file.ToUri());
|
||||
surface.LoadCompleted += (s, e) => loadCompleteTaskSource.TrySetResult();
|
||||
await loadCompleteTaskSource.Task.ConfigureAwait(true);
|
||||
return surface;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新视觉对象
|
||||
/// </summary>
|
||||
/// <param name="spriteVisual">拼合视觉</param>
|
||||
protected virtual void OnUpdateVisual(SpriteVisual spriteVisual)
|
||||
protected virtual void UpdateVisual(SpriteVisual spriteVisual)
|
||||
{
|
||||
spriteVisual.Size = ActualSize;
|
||||
}
|
||||
@@ -158,15 +158,28 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
|
||||
if (imageSurface != null)
|
||||
{
|
||||
spriteVisual = CompositeSpriteVisual(compositor, imageSurface);
|
||||
OnUpdateVisual(spriteVisual);
|
||||
using (imageSurface)
|
||||
{
|
||||
spriteVisual = CompositeSpriteVisual(compositor, imageSurface);
|
||||
UpdateVisual(spriteVisual);
|
||||
|
||||
ElementCompositionPreview.SetElementChildVisual(this, spriteVisual);
|
||||
await ShowAsync(token).ConfigureAwait(true);
|
||||
ElementCompositionPreview.SetElementChildVisual(this, spriteVisual);
|
||||
await ShowAsync(token).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
|
||||
{
|
||||
surfaceLoadTaskCompletionSource = new();
|
||||
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(file.ToUri());
|
||||
surface.LoadCompleted += loadedImageSourceLoadCompletedEventHandler;
|
||||
await surfaceLoadTaskCompletionSource.Task.ConfigureAwait(true);
|
||||
LoadImageSurfaceCompleted(surface);
|
||||
return surface;
|
||||
}
|
||||
|
||||
private async Task ShowAsync(CancellationToken token)
|
||||
{
|
||||
if (!isShow)
|
||||
@@ -209,11 +222,27 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoadImageSurfaceLoadCompleted(LoadedImageSurface surface, LoadedImageSourceLoadCompletedEventArgs e)
|
||||
{
|
||||
surfaceLoadTaskCompletionSource?.TrySetResult();
|
||||
surface.LoadCompleted -= loadedImageSourceLoadCompletedEventHandler;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (e.NewSize != e.PreviousSize && spriteVisual != null)
|
||||
{
|
||||
OnUpdateVisual(spriteVisual);
|
||||
UpdateVisual(spriteVisual);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUnload(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Unloading();
|
||||
spriteVisual?.Dispose();
|
||||
spriteVisual = null;
|
||||
|
||||
SizeChanged -= sizeChangedEventHandler;
|
||||
Unloaded -= unloadEventHandler;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ internal sealed partial class Gradient : CompositionImage
|
||||
private double imageAspectRatio;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnUpdateVisual(SpriteVisual? spriteVisual)
|
||||
protected override void UpdateVisual(SpriteVisual? spriteVisual)
|
||||
{
|
||||
if (spriteVisual is null)
|
||||
{
|
||||
@@ -31,15 +31,9 @@ internal sealed partial class Gradient : CompositionImage
|
||||
spriteVisual.Size = ActualSize;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
|
||||
protected override void LoadImageSurfaceCompleted(LoadedImageSurface surface)
|
||||
{
|
||||
TaskCompletionSource loadCompleteTaskSource = new();
|
||||
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(file.ToUri());
|
||||
surface.LoadCompleted += (_, _) => loadCompleteTaskSource.TrySetResult();
|
||||
await loadCompleteTaskSource.Task.ConfigureAwait(true);
|
||||
imageAspectRatio = surface.NaturalSize.AspectRatio();
|
||||
return surface;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Control.Theme;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Image;
|
||||
|
||||
@@ -16,6 +17,8 @@ namespace Snap.Hutao.Control.Image;
|
||||
[HighQuality]
|
||||
internal sealed class MonoChrome : CompositionImage
|
||||
{
|
||||
private readonly TypedEventHandler<FrameworkElement, object> actualThemeChangedEventHandler;
|
||||
|
||||
private CompositionColorBrush? backgroundBrush;
|
||||
|
||||
/// <summary>
|
||||
@@ -23,7 +26,8 @@ internal sealed class MonoChrome : CompositionImage
|
||||
/// </summary>
|
||||
public MonoChrome()
|
||||
{
|
||||
ActualThemeChanged += OnActualThemeChanged;
|
||||
actualThemeChangedEventHandler = OnActualThemeChanged;
|
||||
ActualThemeChanged += actualThemeChangedEventHandler;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -41,6 +45,16 @@ internal sealed class MonoChrome : CompositionImage
|
||||
return compositor.CompositeSpriteVisual(alphaMaskEffectBrush);
|
||||
}
|
||||
|
||||
protected override void Unloading()
|
||||
{
|
||||
ActualThemeChanged -= actualThemeChangedEventHandler;
|
||||
|
||||
backgroundBrush?.Dispose();
|
||||
backgroundBrush = null;
|
||||
|
||||
base.Unloading();
|
||||
}
|
||||
|
||||
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
if (backgroundBrush != null)
|
||||
|
||||
@@ -5,27 +5,26 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
Name="RootSplitButton"
|
||||
Padding="0,6"
|
||||
Click="SplitButtonClick"
|
||||
Loaded="OnRootControlLoaded"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<SplitButton.Content>
|
||||
<FontIcon Name="IconPresenter" Glyph=""/>
|
||||
</SplitButton.Content>
|
||||
|
||||
<SplitButton.Flyout>
|
||||
<MenuFlyout>
|
||||
<RadioMenuFlyoutItem
|
||||
Click="RadioMenuFlyoutItemClick"
|
||||
Click="OnMenuItemClick"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Tag="List"
|
||||
Text="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
|
||||
<RadioMenuFlyoutItem
|
||||
Click="RadioMenuFlyoutItemClick"
|
||||
Click="OnMenuItemClick"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Tag="Grid"
|
||||
Text="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>
|
||||
</MenuFlyout>
|
||||
</SplitButton.Flyout>
|
||||
|
||||
</SplitButton>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
@@ -16,12 +17,28 @@ internal sealed partial class PanelSelector : SplitButton
|
||||
|
||||
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), List, OnCurrentChanged);
|
||||
|
||||
private readonly RoutedEventHandler loadedEventHandler;
|
||||
private readonly RoutedEventHandler unloadedEventHandler;
|
||||
private readonly TypedEventHandler<SplitButton, SplitButtonClickEventArgs> clickEventHandler;
|
||||
private readonly RoutedEventHandler menuItemClickEventHandler;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的面板选择器
|
||||
/// </summary>
|
||||
public PanelSelector()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
loadedEventHandler = OnRootLoaded;
|
||||
Loaded += loadedEventHandler;
|
||||
|
||||
clickEventHandler = OnRootClick;
|
||||
Click += clickEventHandler;
|
||||
|
||||
menuItemClickEventHandler = OnMenuItemClick;
|
||||
|
||||
unloadedEventHandler = OnRootUnload;
|
||||
Unloaded += unloadedEventHandler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -33,37 +50,54 @@ internal sealed partial class PanelSelector : SplitButton
|
||||
set => SetValue(CurrentProperty, value);
|
||||
}
|
||||
|
||||
private static void OnCurrentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||
private static void InitializeItems(PanelSelector selector)
|
||||
{
|
||||
OnCurrentChanged((PanelSelector)obj, (string)args.NewValue);
|
||||
MenuFlyout menuFlyout = (MenuFlyout)selector.Flyout;
|
||||
int hash = selector.GetHashCode();
|
||||
foreach (RadioMenuFlyoutItem item in menuFlyout.Items.Cast<RadioMenuFlyoutItem>())
|
||||
{
|
||||
item.GroupName = $"{nameof(PanelSelector)}GroupOf@{hash}";
|
||||
item.Click += selector.menuItemClickEventHandler;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnCurrentChanged(PanelSelector sender, string current)
|
||||
private static void OnCurrentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||
{
|
||||
MenuFlyout menuFlyout = (MenuFlyout)sender.RootSplitButton.Flyout;
|
||||
RadioMenuFlyoutItem targetItem = menuFlyout.Items
|
||||
.Cast<RadioMenuFlyoutItem>()
|
||||
UpdateRootGlyphAndItemIsCheck((PanelSelector)obj, (string)args.NewValue);
|
||||
}
|
||||
|
||||
private static void UpdateRootGlyphAndItemIsCheck(PanelSelector sender, string current)
|
||||
{
|
||||
RadioMenuFlyoutItem targetItem = (RadioMenuFlyoutItem)((MenuFlyout)sender.Flyout).Items
|
||||
.Single(i => (string)i.Tag == current);
|
||||
|
||||
targetItem.IsChecked = true;
|
||||
sender.IconPresenter.Glyph = ((FontIcon)targetItem.Icon).Glyph;
|
||||
}
|
||||
|
||||
private void OnRootControlLoaded(object sender, RoutedEventArgs e)
|
||||
private void OnRootLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// because the GroupName shares in global
|
||||
// we have to implement a control scoped GroupName.
|
||||
PanelSelector selector = (PanelSelector)sender;
|
||||
MenuFlyout menuFlyout = (MenuFlyout)selector.RootSplitButton.Flyout;
|
||||
int hash = GetHashCode();
|
||||
foreach (RadioMenuFlyoutItem item in menuFlyout.Items.Cast<RadioMenuFlyoutItem>())
|
||||
{
|
||||
item.GroupName = $"{nameof(PanelSelector)}GroupOf@{hash}";
|
||||
}
|
||||
|
||||
OnCurrentChanged(selector, Current);
|
||||
InitializeItems(selector);
|
||||
UpdateRootGlyphAndItemIsCheck(selector, Current);
|
||||
}
|
||||
|
||||
private void SplitButtonClick(SplitButton sender, SplitButtonClickEventArgs args)
|
||||
private void OnRootUnload(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Loaded -= loadedEventHandler;
|
||||
Click -= clickEventHandler;
|
||||
|
||||
foreach (MenuFlyoutItemBase item in ((MenuFlyout)((PanelSelector)sender).Flyout).Items)
|
||||
{
|
||||
((RadioMenuFlyoutItem)item).Click -= menuItemClickEventHandler;
|
||||
}
|
||||
|
||||
Unloaded -= unloadedEventHandler;
|
||||
}
|
||||
|
||||
private void OnRootClick(SplitButton sender, SplitButtonClickEventArgs args)
|
||||
{
|
||||
MenuFlyout menuFlyout = (MenuFlyout)sender.Flyout;
|
||||
|
||||
@@ -84,9 +118,8 @@ internal sealed partial class PanelSelector : SplitButton
|
||||
Current = (string)item.Tag;
|
||||
}
|
||||
|
||||
private void RadioMenuFlyoutItemClick(object sender, RoutedEventArgs e)
|
||||
private void OnMenuItemClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)sender;
|
||||
Current = (string)item.Tag;
|
||||
Current = (string)((FrameworkElement)sender).Tag;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ namespace Snap.Hutao.Control;
|
||||
/// </summary>
|
||||
/// <typeparam name="TOwner">所有者的类型</typeparam>
|
||||
[HighQuality]
|
||||
[Obsolete("Use DependencyPropertyAttribute whenever possible")]
|
||||
internal static class Property<TOwner>
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -20,6 +20,7 @@ internal class ScopedPage : Page
|
||||
// Allow GC to Collect the IServiceScope
|
||||
private static readonly WeakReference<IServiceScope> PreviousScopeReference = new(null!);
|
||||
|
||||
private readonly RoutedEventHandler unloadEventHandler;
|
||||
private readonly CancellationTokenSource viewCancellationTokenSource = new();
|
||||
private readonly IServiceScope currentScope;
|
||||
|
||||
@@ -28,7 +29,8 @@ internal class ScopedPage : Page
|
||||
/// </summary>
|
||||
protected ScopedPage()
|
||||
{
|
||||
Unloaded += OnScopedPageUnloaded;
|
||||
unloadEventHandler = OnUnloaded;
|
||||
Unloaded += unloadEventHandler;
|
||||
currentScope = Ioc.Default.CreateScope();
|
||||
DisposePreviousScope();
|
||||
|
||||
@@ -47,19 +49,6 @@ internal class ScopedPage : Page
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化
|
||||
/// 应当在 InitializeComponent() 前调用
|
||||
/// </summary>
|
||||
/// <typeparam name="TViewModel">视图模型类型</typeparam>
|
||||
protected void InitializeWith<TViewModel>()
|
||||
where TViewModel : class, IViewModel
|
||||
{
|
||||
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
viewModel.CancellationToken = viewCancellationTokenSource.Token;
|
||||
DataContext = viewModel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步通知接收器
|
||||
/// </summary>
|
||||
@@ -75,6 +64,19 @@ internal class ScopedPage : Page
|
||||
extra.NotifyNavigationCompleted();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化
|
||||
/// 应当在 InitializeComponent() 前调用
|
||||
/// </summary>
|
||||
/// <typeparam name="TViewModel">视图模型类型</typeparam>
|
||||
protected void InitializeWith<TViewModel>()
|
||||
where TViewModel : class, IViewModel
|
||||
{
|
||||
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
viewModel.CancellationToken = viewCancellationTokenSource.Token;
|
||||
DataContext = viewModel;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
||||
{
|
||||
@@ -105,9 +107,9 @@ internal class ScopedPage : Page
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScopedPageUnloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DataContext = null;
|
||||
Unloaded -= OnScopedPageUnloaded;
|
||||
Unloaded -= unloadEventHandler;
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,10 @@ using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Documents;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Control.Media;
|
||||
using Snap.Hutao.Control.Theme;
|
||||
using Windows.Foundation;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.Text;
|
||||
@@ -29,19 +31,22 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
private static readonly int ItalicTagFullLength = "<i></i>".Length;
|
||||
private static readonly int ItalicTagLeftLength = "<i>".Length;
|
||||
|
||||
private readonly TypedEventHandler<FrameworkElement, object> actualThemeChangedEventHandler;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的呈现描述文本的文本块
|
||||
/// </summary>
|
||||
public DescriptionTextBlock()
|
||||
{
|
||||
IsTabStop = false;
|
||||
this.DisableInteraction();
|
||||
|
||||
Content = new TextBlock()
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
|
||||
ActualThemeChanged += OnActualThemeChanged;
|
||||
actualThemeChangedEventHandler = OnActualThemeChanged;
|
||||
ActualThemeChanged += actualThemeChangedEventHandler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,47 +60,47 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
|
||||
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
TextBlock text = (TextBlock)((DescriptionTextBlock)d).Content;
|
||||
TextBlock textBlock = (TextBlock)((DescriptionTextBlock)d).Content;
|
||||
ReadOnlySpan<char> description = (string)e.NewValue;
|
||||
|
||||
UpdateDescription(text, description);
|
||||
UpdateDescription(textBlock, description);
|
||||
}
|
||||
|
||||
private static void UpdateDescription(TextBlock text, in ReadOnlySpan<char> description)
|
||||
private static void UpdateDescription(TextBlock textBlock, in ReadOnlySpan<char> description)
|
||||
{
|
||||
text.Inlines.Clear();
|
||||
textBlock.Inlines.Clear();
|
||||
|
||||
int last = 0;
|
||||
for (int i = 0; i < description.Length;)
|
||||
{
|
||||
// newline
|
||||
if (description[i..].IndexOf(@"\n") is 0)
|
||||
if (description[i..].StartsWith(@"\n"))
|
||||
{
|
||||
AppendText(text, description[last..i]);
|
||||
AppendLineBreak(text);
|
||||
AppendText(textBlock, description[last..i]);
|
||||
AppendLineBreak(textBlock);
|
||||
i += 1;
|
||||
last = i;
|
||||
}
|
||||
|
||||
// color tag
|
||||
else if (description[i..].IndexOf("<c") is 0)
|
||||
else if (description[i..].StartsWith("<c"))
|
||||
{
|
||||
AppendText(text, description[last..i]);
|
||||
AppendText(textBlock, description[last..i]);
|
||||
Rgba32 color = new(description.Slice(i + 8, 8).ToString());
|
||||
int length = description[(i + ColorTagLeftLength)..].IndexOf('<');
|
||||
AppendColorText(text, description.Slice(i + ColorTagLeftLength, length), color);
|
||||
AppendColorText(textBlock, description.Slice(i + ColorTagLeftLength, length), color);
|
||||
|
||||
i += length + ColorTagFullLength;
|
||||
last = i;
|
||||
}
|
||||
|
||||
// italic
|
||||
else if (description[i..].IndexOf("<i") is 0)
|
||||
else if (description[i..].StartsWith("<i"))
|
||||
{
|
||||
AppendText(text, description[last..i]);
|
||||
AppendText(textBlock, description[last..i]);
|
||||
|
||||
int length = description[(i + ItalicTagLeftLength)..].IndexOf('<');
|
||||
AppendItalicText(text, description.Slice(i + ItalicTagLeftLength, length));
|
||||
AppendItalicText(textBlock, description.Slice(i + ItalicTagLeftLength, length));
|
||||
|
||||
i += length + ItalicTagFullLength;
|
||||
last = i;
|
||||
@@ -107,7 +112,7 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
|
||||
if (i == description.Length - 1)
|
||||
{
|
||||
AppendText(text, description[last..(i + 1)]);
|
||||
AppendText(textBlock, description[last..(i + 1)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,6 +131,7 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make lighter in light mode
|
||||
HslColor hsl = color.ToHsl();
|
||||
hsl.L *= 0.3;
|
||||
targetColor = Rgba32.FromHsl(hsl);
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
<Style BasedOn="{StaticResource DefaultScrollViewerStyle}" TargetType="ScrollViewer">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
</Style>
|
||||
|
||||
<!-- TODO: When will DefaultInfoBarStyle added -->
|
||||
<Style TargetType="InfoBar">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
</Style>
|
||||
|
||||
@@ -25,9 +25,9 @@ internal abstract class ValueConverter<TFrom, TTo> : IValueConverter
|
||||
Ioc.Default
|
||||
.GetRequiredService<ILogger<ValueConverter<TFrom, TTo>>>()
|
||||
.LogError(ex, "值转换器异常");
|
||||
}
|
||||
|
||||
return null;
|
||||
throw;
|
||||
}
|
||||
#else
|
||||
return Convert((TFrom)value);
|
||||
#endif
|
||||
|
||||
@@ -35,7 +35,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
};
|
||||
|
||||
private readonly ILogger logger;
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
|
||||
@@ -50,7 +50,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
public ImageCache(IServiceProvider serviceProvider)
|
||||
{
|
||||
logger = serviceProvider.GetRequiredService<ILogger<ImageCache>>();
|
||||
httpClient = serviceProvider.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(ImageCache));
|
||||
httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
|
||||
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
@@ -58,20 +58,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
/// <inheritdoc/>
|
||||
public void RemoveInvalid()
|
||||
{
|
||||
string folder = GetCacheFolder();
|
||||
string[] files = Directory.GetFiles(folder);
|
||||
|
||||
List<string> filesToDelete = new();
|
||||
|
||||
foreach (string file in files)
|
||||
{
|
||||
if (IsFileInvalid(file, false))
|
||||
{
|
||||
filesToDelete.Add(file);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveInternal(filesToDelete);
|
||||
RemoveInternal(Directory.GetFiles(GetCacheFolder()).Where(file => IsFileInvalid(file, false)));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -92,11 +79,10 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
string[] files = Directory.GetFiles(folder);
|
||||
|
||||
List<string> filesToDelete = new();
|
||||
|
||||
foreach (Uri uri in uriForCachedItems)
|
||||
foreach (ref readonly Uri uri in uriForCachedItems)
|
||||
{
|
||||
string filePath = Path.Combine(folder, GetCacheFileName(uri));
|
||||
if (Array.IndexOf(files, filePath) >= 0)
|
||||
if (files.Contains(filePath))
|
||||
{
|
||||
filesToDelete.Add(filePath);
|
||||
}
|
||||
@@ -184,6 +170,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
logger.LogInformation("Begin downloading for {Uri}", uri);
|
||||
|
||||
int retryCount = 0;
|
||||
HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
|
||||
while (retryCount < 6)
|
||||
{
|
||||
using (HttpResponseMessage message = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||
|
||||
@@ -46,6 +46,7 @@ internal sealed class ScopedDbCurrent<TEntity, TMessage>
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Troubeshooting why the serviceProvider will NRE
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
@@ -25,13 +25,7 @@ internal sealed class ExceptionFormat
|
||||
|
||||
foreach (DictionaryEntry entry in exception.Data)
|
||||
{
|
||||
builder
|
||||
.Append(entry.Key)
|
||||
.Append(":[")
|
||||
.Append(TypeNameHelper.GetTypeDisplayName(entry.Value))
|
||||
.Append("]:")
|
||||
.Append(entry.Value)
|
||||
.Append(StringLiterals.CRLF);
|
||||
builder.AppendLine($"{entry.Key}:[{TypeNameHelper.GetTypeDisplayName(entry.Value)}]:entry.Value");
|
||||
}
|
||||
|
||||
builder.AppendLine(SectionSeparator);
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Snap.Hutao.Core.Windowing;
|
||||
/// </summary>
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
[SuppressMessage("", "CA1001")]
|
||||
internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessage>
|
||||
internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutStateChangedMessage>
|
||||
where TWindow : Window, IWindowOptionsSource
|
||||
{
|
||||
private readonly TWindow window;
|
||||
@@ -53,7 +53,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutOpenCloseMessag
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Receive(FlyoutOpenCloseMessage message)
|
||||
public void Receive(FlyoutStateChangedMessage message)
|
||||
{
|
||||
UpdateDragRectangles(message.IsOpen);
|
||||
}
|
||||
|
||||
@@ -7,17 +7,22 @@ namespace Snap.Hutao.Message;
|
||||
/// Flyout开启关闭消息
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class FlyoutOpenCloseMessage
|
||||
internal sealed class FlyoutStateChangedMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的Flyout开启关闭消息
|
||||
/// </summary>
|
||||
/// <param name="isOpen">是否为开启状态</param>
|
||||
public FlyoutOpenCloseMessage(bool isOpen)
|
||||
public FlyoutStateChangedMessage(bool isOpen)
|
||||
{
|
||||
IsOpen = isOpen;
|
||||
}
|
||||
|
||||
public static FlyoutStateChangedMessage Open { get; } = new(true);
|
||||
|
||||
public static FlyoutStateChangedMessage Close { get; } = new(false);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 是否为开启状态
|
||||
/// </summary>
|
||||
Reference in New Issue
Block a user