add system backdrop mica

This commit is contained in:
DismissedLight
2022-07-07 15:48:52 +08:00
parent dbad51d52f
commit 344e5cd31b
35 changed files with 1004 additions and 58 deletions

View File

@@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.1" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.2" />
</ItemGroup>
<ItemGroup>

View File

@@ -5,6 +5,7 @@
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>

View File

@@ -62,24 +62,24 @@ Global
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|Any CPU.Build.0 = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|arm64.ActiveCfg = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|arm64.Build.0 = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|x64.ActiveCfg = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|x64.Build.0 = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|x64.ActiveCfg = Release|x64
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|x64.Build.0 = Release|x64
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|x86.ActiveCfg = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|x86.Build.0 = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.ActiveCfg = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.ActiveCfg = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.ActiveCfg = Debug|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.Build.0 = Debug|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x86.ActiveCfg = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x86.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|Any CPU.Build.0 = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|arm64.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|arm64.Build.0 = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.ActiveCfg = Release|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.HostBackdrop;
/// <summary>
/// 回退行为
/// </summary>
public enum BackbdropFallBackBehavior
{
/// <summary>
/// 回退到无
/// </summary>
None,
/// <summary>
/// 回退到亚克力
/// </summary>
Acrylic,
}

View File

@@ -0,0 +1,184 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
using System.Runtime.InteropServices;
using Windows.System;
using WinRT;
namespace Snap.Hutao.Control.HostBackdrop;
/// <summary>
/// 系统背景帮助类
/// </summary>
public class SystemBackdrop
{
private readonly Window window;
private readonly BackbdropFallBackBehavior fallBackBehavior;
private WindowsSystemDispatcherQueueHelper? dispatcherQueueHelper;
private ISystemBackdropControllerWithTargets? backdropController;
private SystemBackdropConfiguration? configurationSource;
/// <summary>
/// 构造一个新的系统背景帮助类
/// </summary>
/// <param name="window">窗体</param>
/// <param name="fallBackBehavior">回退行为</param>
public SystemBackdrop(Window window, BackbdropFallBackBehavior fallBackBehavior = BackbdropFallBackBehavior.Acrylic)
{
this.window = window;
this.fallBackBehavior = fallBackBehavior;
}
private enum BackDropType
{
None,
Acrylic,
Mica,
}
/// <summary>
/// 尝试设置背景
/// </summary>
/// <returns>是否设置成功</returns>
public bool TrySetBackdrop()
{
BackDropType targetBackDropType = ResolveBackdropType();
if (targetBackDropType == BackDropType.None)
{
return false;
}
else
{
dispatcherQueueHelper = new WindowsSystemDispatcherQueueHelper();
dispatcherQueueHelper.EnsureWindowsSystemDispatcherQueueController();
// Hooking up the policy object
configurationSource = new SystemBackdropConfiguration();
window.Activated += WindowActivated;
window.Closed += WindowClosed;
((FrameworkElement)window.Content).ActualThemeChanged += WindowThemeChanged;
// Initial configuration state.
configurationSource.IsInputActive = true;
SetConfigurationSourceTheme();
backdropController = targetBackDropType switch
{
BackDropType.Mica => new MicaController(),
BackDropType.Acrylic => new DesktopAcrylicController(),
_ => throw Must.NeverHappen(),
};
ICompositionSupportsSystemBackdrop target = window.As<ICompositionSupportsSystemBackdrop>();
backdropController.AddSystemBackdropTarget(target);
backdropController.SetSystemBackdropConfiguration(configurationSource);
return true;
}
}
private BackDropType ResolveBackdropType()
{
BackDropType targetBackDropType = BackDropType.None;
if (MicaController.IsSupported())
{
targetBackDropType = BackDropType.Mica;
}
else
{
if (fallBackBehavior == BackbdropFallBackBehavior.Acrylic)
{
if (DesktopAcrylicController.IsSupported())
{
targetBackDropType = BackDropType.Acrylic;
}
}
}
return targetBackDropType;
}
private void WindowActivated(object sender, WindowActivatedEventArgs args)
{
Must.NotNull(configurationSource!);
configurationSource.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
}
private void WindowClosed(object sender, WindowEventArgs args)
{
// Make sure any Mica/Acrylic controller is disposed so it doesn't try to
// use this closed window.
if (backdropController != null)
{
backdropController.Dispose();
backdropController = null;
}
window.Activated -= WindowActivated;
configurationSource = null;
}
private void WindowThemeChanged(FrameworkElement sender, object args)
{
if (configurationSource != null)
{
SetConfigurationSourceTheme();
}
}
private void SetConfigurationSourceTheme()
{
Must.NotNull(configurationSource!).Theme = ((FrameworkElement)window.Content).ActualTheme switch
{
ElementTheme.Dark => SystemBackdropTheme.Dark,
ElementTheme.Light => SystemBackdropTheme.Light,
ElementTheme.Default => SystemBackdropTheme.Default,
_ => throw Must.NeverHappen(),
};
}
private class WindowsSystemDispatcherQueueHelper
{
private object dispatcherQueueController = null!;
/// <summary>
/// 确保系统调度队列控制器存在
/// </summary>
public void EnsureWindowsSystemDispatcherQueueController()
{
if (DispatcherQueue.GetForCurrentThread() != null)
{
// one already exists, so we'll just use it.
return;
}
if (dispatcherQueueController == null)
{
DispatcherQueueOptions options;
options.DwSize = Marshal.SizeOf(typeof(DispatcherQueueOptions));
options.ThreadType = 2; // DQTYPE_THREAD_CURRENT
options.ApartmentType = 2; // DQTAT_COM_STA
_ = CreateDispatcherQueueController(options, ref dispatcherQueueController!);
}
}
[DllImport("CoreMessaging.dll")]
private static extern int CreateDispatcherQueueController(
[In] DispatcherQueueOptions options,
[In, Out, MarshalAs(UnmanagedType.IUnknown)] ref object dispatcherQueueController);
[StructLayout(LayoutKind.Sequential)]
private struct DispatcherQueueOptions
{
internal int DwSize;
internal int ThreadType;
internal int ApartmentType;
}
}
}

View File

@@ -81,4 +81,4 @@ internal static class Property<TOwner>
{
return DependencyProperty.RegisterAttached(name, typeof(TProperty), typeof(TOwner), new(callback));
}
}
}

View File

@@ -12,4 +12,24 @@ internal static class SettingKeys
/// 上次打开时App的版本
/// </summary>
public const string LastAppVersion = "LastAppVersion";
/// <summary>
/// 窗体左侧
/// </summary>
public const string WindowLeft = "WindowLeft";
/// <summary>
/// 窗体顶部
/// </summary>
public const string WindowTop = "WindowTop";
/// <summary>
/// 窗体右侧
/// </summary>
public const string WindowRight = "WindowRight";
/// <summary>
/// 窗体底部
/// </summary>
public const string WindowBottom = "WindowBottom";
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
namespace Snap.Hutao.Core.Win32;
[SuppressMessage("", "SA1600")]
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
public POINT(int x, int y)
{
this.X = x;
this.Y = y;
}
public static implicit operator System.Drawing.Point(POINT p)
{
return new System.Drawing.Point(p.X, p.Y);
}
public static implicit operator POINT(System.Drawing.Point p)
{
return new POINT(p.X, p.Y);
}
}

View File

@@ -0,0 +1,129 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Globalization;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Core.Win32;
[SuppressMessage("", "SA1132")]
[SuppressMessage("", "SA1600")]
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left, Top, Right, Bottom;
public RECT(int left, int top, int right, int bottom)
{
Left = left;
Top = top;
Right = right;
Bottom = bottom;
}
public RECT(System.Drawing.Rectangle r)
: this(r.Left, r.Top, r.Right, r.Bottom)
{
}
public int X
{
get => Left;
set
{
Right -= Left - value;
Left = value;
}
}
public int Y
{
get => Top;
set
{
Bottom -= Top - value;
Top = value;
}
}
public int Height
{
get => Bottom - Top;
set => Bottom = value + Top;
}
public int Width
{
get => Right - Left;
set => Right = value + Left;
}
public System.Drawing.Point Location
{
get => new(Left, Top);
set
{
X = value.X;
Y = value.Y;
}
}
public System.Drawing.Size Size
{
get => new(Width, Height);
set
{
Width = value.Width;
Height = value.Height;
}
}
public static implicit operator System.Drawing.Rectangle(RECT r)
{
return new System.Drawing.Rectangle(r.Left, r.Top, r.Width, r.Height);
}
public static implicit operator RECT(System.Drawing.Rectangle r)
{
return new RECT(r);
}
public static bool operator ==(RECT r1, RECT r2)
{
return r1.Equals(r2);
}
public static bool operator !=(RECT r1, RECT r2)
{
return !r1.Equals(r2);
}
public bool Equals(RECT r)
{
return r.Left == Left && r.Top == Top && r.Right == Right && r.Bottom == Bottom;
}
public override bool Equals(object? obj)
{
if (obj is RECT rect)
{
return Equals(rect);
}
else if (obj is System.Drawing.Rectangle rectangle)
{
return Equals(new RECT(rectangle));
}
return false;
}
public override int GetHashCode()
{
return ((System.Drawing.Rectangle)this).GetHashCode();
}
public override string ToString()
{
return string.Format(CultureInfo.CurrentCulture, "{{Left={0},Top={1},Right={2},Bottom={3}}}", Left, Top, Right, Bottom);
}
}

View File

@@ -0,0 +1,89 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Win32;
[SuppressMessage("", "SA1600")]
public enum ShowWindowCommand
{
/// <summary>
/// Hides the window and activates another window.
/// </summary>
Hide = 0,
/// <summary>
/// Activates and displays a window. If the window is minimized or
/// maximized, the system restores it to its original size and position.
/// An application should specify this flag when displaying the window
/// for the first time.
/// </summary>
Normal = 1,
/// <summary>
/// Activates the window and displays it as a minimized window.
/// </summary>
ShowMinimized = 2,
/// <summary>
/// Maximizes the specified window.
/// </summary>
Maximize = 3, // is this the right value?
/// <summary>
/// Activates the window and displays it as a maximized window.
/// </summary>
ShowMaximized = 3,
/// <summary>
/// Displays a window in its most recent size and position. This value
/// is similar to <see cref="Win32.ShowWindowCommand.Normal"/>, except
/// the window is not activated.
/// </summary>
ShowNoActivate = 4,
/// <summary>
/// Activates the window and displays it in its current size and position.
/// </summary>
Show = 5,
/// <summary>
/// Minimizes the specified window and activates the next top-level
/// window in the Z order.
/// </summary>
Minimize = 6,
/// <summary>
/// Displays the window as a minimized window. This value is similar to
/// <see cref="Win32.ShowWindowCommand.ShowMinimized"/>, except the
/// window is not activated.
/// </summary>
ShowMinNoActive = 7,
/// <summary>
/// Displays the window in its current size and position. This value is
/// similar to <see cref="Win32.ShowWindowCommand.Show"/>, except the
/// window is not activated.
/// </summary>
ShowNA = 8,
/// <summary>
/// Activates and displays the window. If the window is minimized or
/// maximized, the system restores it to its original size and position.
/// An application should specify this flag when restoring a minimized window.
/// </summary>
Restore = 9,
/// <summary>
/// Sets the show state based on the SW_* value specified in the
/// STARTUPINFO structure passed to the CreateProcess function by the
/// program that started the application.
/// </summary>
ShowDefault = 10,
/// <summary>
/// <b>Windows 2000/XP:</b> Minimizes a window, even if the thread
/// that owns the window is not responding. This flag should only be
/// used when minimizing windows from a different thread.
/// </summary>
ForceMinimize = 11,
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
namespace Snap.Hutao.Core.Win32;
/// <summary>
/// 包含 user32.dll 平台调用的代码
/// </summary>
internal static class User32
{
[SuppressMessage("", "SA1600")]
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl);
/// <summary>
/// Sets the show state and the restored, minimized, and maximized positions of the specified window.
/// </summary>
/// <param name="hWnd">
/// A handle to the window.
/// </param>
/// <param name="lpwndpl">
/// A pointer to a WINDOWPLACEMENT structure that specifies the new show state and window positions.
/// <para>
/// Before calling SetWindowPlacement, set the length member of the WINDOWPLACEMENT structure to sizeof(WINDOWPLACEMENT). SetWindowPlacement fails if the length member is not set correctly.
/// </para>
/// </param>
/// <returns>
/// If the function succeeds, the return value is nonzero.
/// <para>
/// If the function fails, the return value is zero. To get extended error information, call GetLastError.
/// </para>
/// </returns>
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetWindowPlacement(IntPtr hWnd, [In] ref WINDOWPLACEMENT lpwndpl);
/// <summary>
/// Changes the text of the specified window's title bar (if it has one). If the specified window is a control, the
/// text of the control is changed. However, SetWindowText cannot change the text of a control in another application.
/// <para>
/// Go to <see href="https://msdn.microsoft.com/en-us/library/windows/desktop/ms633546%28v=vs.85%29.aspx"/> for more
/// information
/// </para>
/// </summary>
/// <param name="hwnd">C++ ( hWnd [in]. Type: HWND )<br />A handle to the window or control whose text is to be changed.</param>
/// <param name="lpString">C++ ( lpString [in, optional]. Type: LPCTSTR )<br />The new title or control text.</param>
/// <returns>
/// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero.<br />
/// To get extended error information, call GetLastError.
/// </returns>
/// <remarks>
/// If the target window is owned by the current process, <see cref="SetWindowText" /> causes a WM_SETTEXT message to
/// be sent to the specified window or control. If the control is a list box control created with the WS_CAPTION style,
/// however, <see cref="SetWindowText" /> sets the text for the control, not for the list box entries.<br />To set the
/// text of a control in another process, send the WM_SETTEXT message directly instead of calling
/// <see cref="SetWindowText" />. The <see cref="SetWindowText" /> function does not expand tab characters (ASCII code
/// 0x09). Tab characters are displayed as vertical bar(|) characters.<br />For an example go to
/// <see href="https://msdn.microsoft.com/en-us/library/windows/desktop/ms644928%28v=vs.85%29.aspx#sending">Sending a Message. </see>
/// </remarks>
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool SetWindowText(IntPtr hwnd, string lpString);
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
namespace Snap.Hutao.Core.Win32;
/// <summary>
/// Contains information about the placement of a window on the screen.
/// </summary>
[Serializable]
[StructLayout(LayoutKind.Sequential)]
internal struct WINDOWPLACEMENT
{
/// <summary>
/// The length of the structure, in bytes. Before calling the GetWindowPlacement or SetWindowPlacement functions, set this member to sizeof(WINDOWPLACEMENT).
/// <para>
/// GetWindowPlacement and SetWindowPlacement fail if this member is not set correctly.
/// </para>
/// </summary>
public int Length;
/// <summary>
/// Specifies flags that control the position of the minimized window and the method by which the window is restored.
/// </summary>
public int Flags;
/// <summary>
/// The current show state of the window.
/// </summary>
public ShowWindowCommand ShowCmd;
/// <summary>
/// The coordinates of the window's upper-left corner when the window is minimized.
/// </summary>
public POINT MinPosition;
/// <summary>
/// The coordinates of the window's upper-left corner when the window is maximized.
/// </summary>
public POINT MaxPosition;
/// <summary>
/// The window's coordinates when the window is in the restored position.
/// </summary>
public RECT NormalPosition;
/// <summary>
/// Gets the default (empty) value.
/// </summary>
public static WINDOWPLACEMENT Default
{
get
{
WINDOWPLACEMENT result = default(WINDOWPLACEMENT);
result.Length = Marshal.SizeOf(result);
return result;
}
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Reflection;
namespace Snap.Hutao.Extension;
/// <summary>
/// 枚举拓展
/// </summary>
public static class EnumExtensions
{
/// <summary>
/// 获取枚举的描述
/// </summary>
/// <typeparam name="TEnum">枚举的类型</typeparam>
/// <param name="enum">枚举值</param>
/// <returns>描述</returns>
public static string GetDescription<TEnum>(this TEnum @enum)
where TEnum : struct, Enum
{
string enumName = Must.NotNull(Enum.GetName(@enum)!);
FieldInfo? field = @enum.GetType().GetField(enumName);
DescriptionAttribute? attr = field?.GetCustomAttribute<DescriptionAttribute>();
return attr?.Description ?? enumName;
}
}

View File

@@ -8,7 +8,7 @@
mc:Ignorable="d"
Closed="MainWindowClosed">
<Grid Background="{ThemeResource SolidBackgroundFillColorBaseBrush}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="48.8"/>
<RowDefinition/>

View File

@@ -3,6 +3,12 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Control.HostBackdrop;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Win32;
using System.Drawing;
using System.Runtime.InteropServices;
using WinRT.Interop;
namespace Snap.Hutao;
@@ -13,22 +19,76 @@ namespace Snap.Hutao;
public sealed partial class MainWindow : Window
{
private readonly AppDbContext appDbContext;
private readonly ILogger<MainWindow> logger;
private readonly IntPtr handle;
/// <summary>
/// 构造一个新的主窗体
/// </summary>
/// <param name="appDbContext">数据库上下文</param>
public MainWindow(AppDbContext appDbContext)
/// <param name="logger">日志器</param>
public MainWindow(AppDbContext appDbContext, ILogger<MainWindow> logger)
{
this.appDbContext = appDbContext;
this.logger = logger;
InitializeComponent();
ExtendsContentIntoTitleBar = true;
SetTitleBar(TitleBarView.DragableArea);
handle = WindowNative.GetWindowHandle(this);
InitializeWindow();
}
private static RECT RetriveWindowRect()
{
int left = LocalSetting.GetValueType<int>(SettingKeys.WindowLeft);
int top = LocalSetting.GetValueType<int>(SettingKeys.WindowTop);
int right = LocalSetting.GetValueType<int>(SettingKeys.WindowRight);
int bottom = LocalSetting.GetValueType<int>(SettingKeys.WindowBottom);
return new RECT(left, top, right, bottom);
}
private void InitializeWindow()
{
RECT rect = RetriveWindowRect();
if (rect.Size.IsEmpty)
{
return;
}
WINDOWPLACEMENT windowPlacement = new()
{
Length = Marshal.SizeOf<WINDOWPLACEMENT>(),
MaxPosition = new Point(-1, -1),
NormalPosition = rect,
ShowCmd = ShowWindowCommand.Normal,
};
User32.SetWindowPlacement(handle, ref windowPlacement);
User32.SetWindowText(handle, "胡桃");
bool micaApplied = new SystemBackdrop(this).TrySetBackdrop();
logger.LogInformation("{name} 设置{result}", nameof(SystemBackdrop), micaApplied ? "成功" : "失败");
}
private void SaveWindowRect()
{
WINDOWPLACEMENT windowPlacement = WINDOWPLACEMENT.Default;
User32.GetWindowPlacement(handle, ref windowPlacement);
LocalSetting.SetValueType(SettingKeys.WindowLeft, windowPlacement.NormalPosition.Left);
LocalSetting.SetValueType(SettingKeys.WindowTop, windowPlacement.NormalPosition.Top);
LocalSetting.SetValueType(SettingKeys.WindowRight, windowPlacement.NormalPosition.Right);
LocalSetting.SetValueType(SettingKeys.WindowBottom, windowPlacement.NormalPosition.Bottom);
}
private void MainWindowClosed(object sender, WindowEventArgs args)
{
SaveWindowRect();
// save datebase
int changes = appDbContext.SaveChanges();
Verify.Operation(changes == 0, "存在可避免的未经处理的数据库更改");

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Annotation;
/// <summary>
/// 枚举的文本描述特性
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
internal class DescriptionAttribute : Attribute
{
/// <summary>
/// 构造一个新的枚举的文本描述特性
/// </summary>
/// <param name="description">描述</param>
public DescriptionAttribute(string description)
{
Description = description;
}
/// <summary>
/// 获取文本描述
/// </summary>
public string Description { get; init; }
}

View File

@@ -17,6 +17,7 @@ public enum FightProperty
/// <summary>
/// 基础生命值
/// </summary>
[Description("基础生命值")]
FIGHT_PROP_BASE_HP = 1,
/// <summary>
@@ -27,11 +28,13 @@ public enum FightProperty
/// <summary>
/// 生命值加成百分比
/// </summary>
[Description("生命值")]
FIGHT_PROP_HP_PERCENT = 3,
/// <summary>
/// 基础攻击力
/// </summary>
[Description("基础攻击力")]
FIGHT_PROP_BASE_ATTACK = 4,
/// <summary>
@@ -42,11 +45,13 @@ public enum FightProperty
/// <summary>
/// 攻击力百分比
/// </summary>
[Description("攻击力")]
FIGHT_PROP_ATTACK_PERCENT = 6,
/// <summary>
/// 基础防御力
/// </summary>
[Description("基础防御力")]
FIGHT_PROP_BASE_DEFENSE = 7,
/// <summary>
@@ -57,6 +62,7 @@ public enum FightProperty
/// <summary>
/// 防御力百分比
/// </summary>
[Description("防御力")]
FIGHT_PROP_DEFENSE_PERCENT = 9,
/// <summary>
@@ -82,6 +88,7 @@ public enum FightProperty
/// <summary>
/// 暴击率
/// </summary>
[Description("暴击率")]
FIGHT_PROP_CRITICAL = 20,
/// <summary>
@@ -92,11 +99,13 @@ public enum FightProperty
/// <summary>
/// 暴击伤害
/// </summary>
[Description("暴击伤害")]
FIGHT_PROP_CRITICAL_HURT = 22,
/// <summary>
/// 元素充能效率
/// </summary>
[Description("元素充能效率")]
FIGHT_PROP_CHARGE_EFFICIENCY = 23,
/// <summary>
@@ -112,6 +121,7 @@ public enum FightProperty
/// <summary>
/// 治疗提升
/// </summary>
[Description("治疗加成")]
FIGHT_PROP_HEAL_ADD = 26,
/// <summary>
@@ -122,6 +132,7 @@ public enum FightProperty
/// <summary>
/// 元素精通
/// </summary>
[Description("元素精通")]
FIGHT_PROP_ELEMENT_MASTERY = 28,
/// <summary>
@@ -132,6 +143,7 @@ public enum FightProperty
/// <summary>
/// 物理伤害加成
/// </summary>
[Description("物理伤害加成")]
FIGHT_PROP_PHYSICAL_ADD_HURT = 30,
/// <summary>
@@ -147,16 +159,19 @@ public enum FightProperty
/// <summary>
/// 火元素伤害加成
/// </summary>
[Description("火元素伤害加成")]
FIGHT_PROP_FIRE_ADD_HURT = 40,
/// <summary>
/// 雷元素伤害加成
/// </summary>
[Description("雷元素伤害加成")]
FIGHT_PROP_ELEC_ADD_HURT = 41,
/// <summary>
/// 水元素伤害加成
/// </summary>
[Description("水元素伤害加成")]
FIGHT_PROP_WATER_ADD_HURT = 42,
/// <summary>
@@ -167,16 +182,19 @@ public enum FightProperty
/// <summary>
/// 风元素伤害加成
/// </summary>
[Description("风元素伤害加成")]
FIGHT_PROP_WIND_ADD_HURT = 44,
/// <summary>
/// 岩元素伤害加成
/// </summary>
[Description("岩元素伤害加成")]
FIGHT_PROP_ROCK_ADD_HURT = 45,
/// <summary>
/// 冰元素伤害加成
/// </summary>
[Description("冰元素伤害加成")]
FIGHT_PROP_ICE_ADD_HURT = 46,
/// <summary>

View File

@@ -8,7 +8,6 @@ namespace Snap.Hutao.Model.Metadata.Avatar;
/// </summary>
public class ProudableSkill : SkillBase
{
/// <summary>
/// 组Id
/// </summary>

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色头像转换器
/// </summary>
internal class IconConverter : IValueConverter
{
private const string BaseUrl = "https://upload-bbs.mihoyo.com/game_record/genshin/character_icon/{0}.png";
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
{
return new Uri(string.Format(BaseUrl, value));
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色侧面头像转换器
/// </summary>
internal class SideIconConverter : IValueConverter
{
private const string BaseUrl = "https://upload-bbs.mihoyo.com/game_record/genshin/character_side_icon/{0}.png";
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
{
return new Uri(string.Format(BaseUrl, value));
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
}
}

View File

@@ -9,7 +9,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.0.6.0" />
Version="1.0.9.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

View File

@@ -31,8 +31,9 @@ public interface IUserService
/// 通常用户是未初始化的
/// </summary>
/// <param name="user">待添加的用户</param>
/// <param name="uid">用户的米游社UID</param>
/// <returns>用户初始化是否成功</returns>
Task<UserAddResult> TryAddUserAsync(User user);
Task<UserAddResult> TryAddUserAsync(User user, string uid);
/// <summary>
/// 异步移除用户

View File

@@ -11,7 +11,12 @@ public enum UserAddResult
/// <summary>
/// 添加成功
/// </summary>
Ok,
Added,
/// <summary>
/// 用户的Cookie成功更新
/// </summary>
Updated,
/// <summary>
/// 已经存在该用户

View File

@@ -69,31 +69,45 @@ internal class UserService : IUserService
}
/// <inheritdoc/>
public async Task<UserAddResult> TryAddUserAsync(User user)
public async Task<UserAddResult> TryAddUserAsync(User newUser, string uid)
{
string? newUsersCookie = user.Cookie;
Must.NotNull(cachedUsers!);
// Prevent users add same account.
bool userAlreadyExists = await appDbContext.Users
.AnyAsync(u => u.Cookie == newUsersCookie)
.ConfigureAwait(false);
// 查找是否有相同的uid
User? userWithSameUid = cachedUsers
.SingleOrDefault(u => u.UserInfo!.Uid == uid);
if (userAlreadyExists)
if (userWithSameUid != null)
{
return UserAddResult.AlreadyExists;
// Prevent users from adding same cookie.
if (userWithSameUid.Cookie == newUser.Cookie)
{
return UserAddResult.AlreadyExists;
}
else
{
// Try update user here.
userWithSameUid.Cookie = newUser.Cookie;
appDbContext.Users.Update(userWithSameUid);
await appDbContext
.SaveChangesAsync()
.ConfigureAwait(false);
return UserAddResult.Updated;
}
}
bool userInitialized = await user
.InitializeAsync(userClient, userGameRoleClient)
.ConfigureAwait(false);
if (userInitialized)
// must continue on the caller thread.
if (await newUser.InitializeAsync(userClient, userGameRoleClient))
{
appDbContext.Users.Add(user);
appDbContext.Users.Add(newUser);
await appDbContext
.SaveChangesAsync()
.ConfigureAwait(false);
return UserAddResult.Ok;
return UserAddResult.Added;
}
return UserAddResult.InitializeFailed;
@@ -111,16 +125,23 @@ internal class UserService : IUserService
{
if (cachedUsers == null)
{
appDbContext.Users.Load();
await appDbContext.Users
.LoadAsync()
.ConfigureAwait(false);
cachedUsers = appDbContext.Users.Local.ToObservableCollection();
foreach (User user in cachedUsers)
{
user.RemoveCommand = removeCommand;
await user.InitializeAsync(userClient, userGameRoleClient);
await user
.InitializeAsync(userClient, userGameRoleClient)
.ConfigureAwait(false);
}
CurrentUser = await appDbContext.Users.SingleOrDefaultAsync(user => user.IsSelected);
CurrentUser = await appDbContext.Users
.SingleOrDefaultAsync(user => user.IsSelected)
.ConfigureAwait(false);
}
return cachedUsers;
@@ -129,15 +150,15 @@ internal class UserService : IUserService
/// <inheritdoc/>
public IDictionary<string, string> ParseCookie(string cookie)
{
Dictionary<string, string> cookieDictionary = new();
SortedDictionary<string, string> cookieDictionary = new();
string[] values = cookie.TrimEnd(';').Split(';');
foreach (string[] parts in values.Select(c => c.Split(new[] { '=' }, 2)))
foreach (string[] parts in values.Select(c => c.Split('=', 2)))
{
string cookieName = parts[0].Trim();
string cookieValue = parts.Length == 1 ? string.Empty : parts[1].Trim();
cookieDictionary[cookieName] = cookieValue;
cookieDictionary.Add(cookieName, cookieValue);
}
return cookieDictionary;

View File

@@ -36,6 +36,7 @@
<None Remove="View\Page\AnnouncementPage.xaml" />
<None Remove="View\Page\SettingPage.xaml" />
<None Remove="View\Page\WelcomePage.xaml" />
<None Remove="View\Page\WikiAvatarPage.xaml" />
<None Remove="View\TitleView.xaml" />
<None Remove="View\UserView.xaml" />
</ItemGroup>
@@ -132,6 +133,11 @@
<ProjectReference Include="..\SettingsUI\SettingsUI.csproj" />
<ProjectReference Include="..\Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\WikiAvatarPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\AchievementPage.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -14,10 +14,11 @@ public sealed partial class UserDialog : ContentDialog
/// <summary>
/// 构造一个新的添加用户对话框
/// </summary>
public UserDialog()
/// <param name="window">呈现的父窗口</param>
public UserDialog(Microsoft.UI.Xaml.Window window)
{
InitializeComponent();
XamlRoot = App.Window!.Content.XamlRoot;
XamlRoot = window.Content.XamlRoot;
}
/// <summary>
@@ -29,7 +30,7 @@ public sealed partial class UserDialog : ContentDialog
ContentDialogResult result = await ShowAsync();
string cookie = InputText.Text;
return new(result != ContentDialogResult.Secondary, cookie);
return new(result == ContentDialogResult.Primary, cookie);
}
private void InputTextChanged(object sender, TextChangedEventArgs e)

View File

@@ -34,6 +34,12 @@
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem Content="角色" helper:NavHelper.NavigateTo="page:WikiAvatarPage">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xE7C4;"/>
</NavigationViewItem.Icon>
</NavigationViewItem>
</NavigationView.MenuItems>
<NavigationView.PaneFooter>

View File

@@ -28,6 +28,6 @@ public sealed partial class MainView : UserControl
navigationService = Ioc.Default.GetRequiredService<INavigationService>();
navigationService.Initialize(NavView, ContentFrame);
navigationService.Navigate<WelcomePage>(INavigationAwaiter.Default, false);
navigationService.Navigate<AnnouncementPage>(INavigationAwaiter.Default, true);
}
}

View File

@@ -0,0 +1,33 @@
<Page
x:Class="Snap.Hutao.View.Page.WikiAvatarPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Snap.Hutao.View.Page"
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"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="Loaded">
<mxic:InvokeCommandAction Command="{Binding OpenUICommand}"/>
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors>
<Grid>
<SplitView IsPaneOpen="True" DisplayMode="Inline">
<SplitView.Pane>
<ListView
SelectionMode="Single"
ItemsSource="{Binding Avatars}"
SelectedItem="{Binding Selected,Mode=TwoWay}"/>
</SplitView.Pane>
<SplitView.Content>
<Grid>
<TextBlock
Text="{Binding Selected.Name}"/>
</Grid>
</SplitView.Content>
</SplitView>
</Grid>
</Page>

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.ViewModel;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
namespace Snap.Hutao.View.Page;
/// <summary>
/// 角色资料页
/// </summary>
public sealed partial class WikiAvatarPage : Microsoft.UI.Xaml.Controls.Page
{
/// <summary>
/// 构造一个新的角色资料页
/// </summary>
public WikiAvatarPage()
{
DataContext = Ioc.Default.GetRequiredService<WikiAvatarViewModel>();
InitializeComponent();
}
}

View File

@@ -35,7 +35,6 @@
Text="{Binding SelectedUser.UserInfo.Nickname,Mode=OneWay}"
TextTrimming="CharacterEllipsis"/>
<Button
x:Name="UsersFlyoutButton"
Background="Transparent"
BorderBrush="{x:Null}"
Height="38.4"
@@ -184,8 +183,7 @@
<AppBarButton
Icon="Add"
Label="添加新用户"
Command="{Binding AddUserCommand}"
CommandParameter="{x:Bind UsersFlyoutButton.Flyout}"/>
Command="{Binding AddUserCommand}"/>
</CommandBar>
</StackPanel>
</Flyout>

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Entity;
@@ -20,6 +19,8 @@ namespace Snap.Hutao.ViewModel;
[Injection(InjectAs.Transient)]
internal class UserViewModel : ObservableObject
{
private const string AccountIdKey = "account_id";
private readonly IUserService userService;
private readonly IInfoBarService infoBarService;
private readonly ICommand removeUserCommandCache;
@@ -39,7 +40,7 @@ internal class UserViewModel : ObservableObject
this.infoBarService = infoBarService;
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
AddUserCommand = asyncRelayCommandFactory.Create<Flyout>(AddUserAsync);
AddUserCommand = asyncRelayCommandFactory.Create(AddUserAsync);
removeUserCommandCache = asyncRelayCommandFactory.Create<User>(RemoveUserAsync);
}
@@ -83,10 +84,10 @@ internal class UserViewModel : ObservableObject
// O(1) to validate cookie
foreach ((string key, string value) in map)
{
if (key == "account_id" || key == "cookie_token" || key == "ltoken" || key == "ltuid")
if (key == AccountIdKey || key == "cookie_token" || key == "ltoken" || key == "ltuid")
{
validFlag--;
filteredCookie[key] = value;
filteredCookie.Add(key, value);
}
}
@@ -107,12 +108,10 @@ internal class UserViewModel : ObservableObject
SelectedUser = userService.CurrentUser;
}
private async Task AddUserAsync(Flyout? flyout)
private async Task AddUserAsync()
{
// hide the flyout, otherwise dialog can't open.
flyout?.Hide();
Result<bool, string> result = await new UserDialog().GetInputCookieAsync();
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
Result<bool, string> result = await new UserDialog(mainWindow).GetInputCookieAsync();
// user confirms the input
if (result.IsOk)
@@ -128,22 +127,27 @@ internal class UserViewModel : ObservableObject
RemoveCommand = removeUserCommandCache,
};
switch (await userService.TryAddUserAsync(user))
switch (await userService.TryAddUserAsync(user, filteredCookie[AccountIdKey]))
{
case UserAddResult.Ok:
case UserAddResult.Added:
infoBarService.Success($"用户 [{user.UserInfo!.Nickname}] 添加成功");
break;
case UserAddResult.Updated:
infoBarService.Success($"用户 [{user.UserInfo!.Nickname}] 更新成功");
break;
case UserAddResult.AlreadyExists:
infoBarService.Information($"用户 [{user.UserInfo!.Nickname}] 已经存在");
break;
case UserAddResult.InitializeFailed:
infoBarService.Warning("此Cookie无法获取用户信息请重新输入");
infoBarService.Warning("此 Cookie 无法获取用户信息,请重新输入");
break;
default:
throw Must.NeverHappen();
}
}
else
{
infoBarService.Warning("提供的字符串并不是有效的Cookie请重新输入");
infoBarService.Warning("提供的文本不是正确的 Cookie ,请重新输入");
}
}
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Service.Metadata;
using System.Collections.Generic;
namespace Snap.Hutao.ViewModel;
/// <summary>
/// 角色资料视图模型
/// </summary>
[Injection(InjectAs.Transient)]
internal class WikiAvatarViewModel : ObservableObject
{
private readonly IMetadataService metadataService;
private List<Avatar>? avatars;
private Avatar? selected;
/// <summary>
/// 构造一个新的角色资料视图模型
/// </summary>
/// <param name="metadataService">元数据服务</param>
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
public WikiAvatarViewModel(IMetadataService metadataService, IAsyncRelayCommandFactory asyncRelayCommandFactory)
{
this.metadataService = metadataService;
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
}
/// <summary>
/// 角色列表
/// </summary>
public List<Avatar>? Avatars { get => avatars; set => SetProperty(ref avatars, value); }
/// <summary>
/// 选中的角色
/// </summary>
public Avatar? Selected { get => selected; set => SetProperty(ref selected, value); }
/// <summary>
/// 打开页面命令
/// </summary>
public ICommand OpenUICommand { get; }
private async Task OpenUIAsync()
{
if (await metadataService.InitializeAsync())
{
IEnumerable<Avatar>? avatars = await metadataService.GetAvatarsAsync();
Avatars = new List<Avatar>(avatars);
}
}
}

View File

@@ -32,7 +32,7 @@ public class Announcement : AnnouncementContent
{
get
{
DateTime now = DateTime.UtcNow + TimeSpan.FromHours(8);
DateTimeOffset now = DateTimeOffset.UtcNow;
// 尚未开始
if (StartTime > now)
@@ -76,7 +76,7 @@ public class Announcement : AnnouncementContent
if (timePercent == 0)
{
// UTC+8
DateTime currentTime = DateTime.UtcNow.AddHours(8);
DateTimeOffset currentTime = DateTimeOffset.UtcNow;
TimeSpan current = currentTime - StartTime;
TimeSpan total = EndTime - StartTime;
timePercent = current / total;

View File

@@ -17,7 +17,7 @@ public struct PlayerUid
/// <param name="region">服务器,当提供该参数时会无条件信任</param>
public PlayerUid(string value, string? region = default)
{
Requires.Argument(value.Length == 9, nameof(value), "uid应为9位数字");
Requires.Argument(value.Length == 9, nameof(value), "uid 应为9位数字");
Value = value;
if (region != null)