From 7861ebf9981e3d5a8bf37df8a3a2a327a7556d6e Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Sat, 18 Nov 2023 22:41:25 +0800 Subject: [PATCH] #impl 1096 --- .../UnsafeRuntimeBehaviorTest.cs | 19 ++ .../Snap.Hutao.Win32/NativeMethods.json | 8 +- .../Snap.Hutao.Win32/NativeMethods.txt | 12 +- src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs | 3 +- src/Snap.Hutao/Snap.Hutao/App.xaml | 1 + .../Control/Helper/ScrollViewerHelper.cs | 14 + .../Control/Theme/ScrollViewer.xaml | 287 ++++++++++++++++++ .../Snap.Hutao/Core/Windowing/BackdropType.cs | 5 - .../Picker/FileSystemPickerInteraction.cs | 211 +++++++++++++ .../FileSystemPickerInteractionExtension.cs | 19 ++ .../Picker/IFileSystemPickerInteraction.cs | 38 +-- .../Factory/Picker/IPickerFactory.cs | 38 --- .../Factory/Picker/PickerFactory.cs | 88 ------ src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs | 4 +- .../Snap.Hutao/Package.appxmanifest | 2 +- .../Snap.Hutao/Resource/Localization/SH.resx | 18 ++ ...ameRecordCharacterAvatarInfoTransformer.cs | 23 +- .../Service/Game/Locator/ManualGameLocator.cs | 19 +- src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 9 +- .../Snap.Hutao/View/Page/AchievementPage.xaml | 8 +- .../Snap.Hutao/View/Page/SettingPage.xaml | 127 ++++---- .../Achievement/AchievementImporter.cs | 10 +- .../Achievement/AchievementViewModel.cs | 18 +- .../ViewModel/GachaLog/GachaLogViewModel.cs | 73 +++-- .../ViewModel/Setting/SettingViewModel.cs | 13 +- 25 files changed, 748 insertions(+), 319 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/Helper/ScrollViewerHelper.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/Theme/ScrollViewer.xaml create mode 100644 src/Snap.Hutao/Snap.Hutao/Factory/Picker/FileSystemPickerInteraction.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Factory/Picker/FileSystemPickerInteractionExtension.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Factory/Picker/IPickerFactory.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Factory/Picker/PickerFactory.cs diff --git a/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs index 0d516772..d876b35b 100644 --- a/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs +++ b/src/Snap.Hutao/Snap.Hutao.Test/RuntimeBehavior/UnsafeRuntimeBehaviorTest.cs @@ -18,4 +18,23 @@ public sealed class UnsafeRuntimeBehaviorTest Assert.AreEqual(uint.MaxValue, *(uint*)pBytes); } } +} + +[TestClass] +public sealed class NewModifierRuntimeBehaviorTest +{ + private interface IBase + { + int GetValue(); + } + + private interface IBaseImpl : IBase + { + new int GetValue(); + } + + private sealed class Impl : IBaseImpl + { + public int GetValue() => 1; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.json b/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.json index bcd1d020..b25812f1 100644 --- a/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.json +++ b/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.json @@ -1,5 +1,11 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json", "allowMarshaling": true, - "useSafeHandles": false + "useSafeHandles": false, + "comInterop": { + "preserveSigMethods": [ + "IFileOpenDialog.Show", + "IFileSaveDialog.Show" + ] + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt b/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt index ee0d767e..6520488d 100644 --- a/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt +++ b/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt @@ -31,6 +31,9 @@ WriteProcessMemory CoCreateInstance CoWaitForMultipleObjects +// SHELL32 +SHCreateItemFromParsingName + // USER32 AttachThreadInput FindWindowExW @@ -47,18 +50,21 @@ SetForegroundWindow UnregisterHotKey // COM +FileOpenDialog +FileSaveDialog +IFileOpenDialog +IFileSaveDialog IPersistFile IShellLinkW ShellLink SHELL_LINK_DATA_FLAGS -FileOpenDialog -IFileOpenDialog // WinRT IMemoryBufferByteAccess // Const value INFINITE +MAX_PATH WM_GETMINMAXINFO WM_HOTKEY WM_NCRBUTTONDOWN @@ -66,6 +72,8 @@ WM_NCRBUTTONUP WM_NULL // Type & Enum definition +HRESULT_FROM_WIN32 +SLGP_FLAGS // System.Threading LPTHREAD_START_ROUTINE diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs b/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs index e5ae3614..a54fa034 100644 --- a/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs +++ b/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs @@ -8,8 +8,7 @@ internal static partial class PInvoke { /// internal static unsafe HRESULT CoCreateInstance(object? pUnkOuter, CLSCTX dwClsContext, out TInterface ppv) -where TInterface : class - + where TInterface : class { HRESULT hr = CoCreateInstance(typeof(TClass).GUID, pUnkOuter, dwClsContext, typeof(TInterface).GUID, out object o); ppv = (TInterface)o; diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml b/src/Snap.Hutao/Snap.Hutao/App.xaml index 590d52a5..b15eeb24 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml @@ -20,6 +20,7 @@ + diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Helper/ScrollViewerHelper.cs b/src/Snap.Hutao/Snap.Hutao/Control/Helper/ScrollViewerHelper.cs new file mode 100644 index 00000000..11f0d0c7 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Helper/ScrollViewerHelper.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Snap.Hutao.Control.Helper; + +[SuppressMessage("", "SH001")] +[DependencyProperty("LeftPanelMaxWidth", typeof(double), IsAttached = true, AttachedType = typeof(ScrollViewer))] +[DependencyProperty("RightPanel", typeof(UIElement), IsAttached = true, AttachedType = typeof(ScrollViewer))] +public sealed partial class ScrollViewerHelper +{ +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/ScrollViewer.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/ScrollViewer.xaml new file mode 100644 index 00000000..d0190712 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/ScrollViewer.xaml @@ -0,0 +1,287 @@ + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/BackdropType.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/BackdropType.cs index d4a0a8c0..f6416860 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/BackdropType.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/BackdropType.cs @@ -9,11 +9,6 @@ namespace Snap.Hutao.Core.Windowing; [HighQuality] internal enum BackdropType { - /// - /// 透明 - /// - Transparent = -1, - /// /// 无 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Picker/FileSystemPickerInteraction.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Picker/FileSystemPickerInteraction.cs new file mode 100644 index 00000000..1c97aebf --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Factory/Picker/FileSystemPickerInteraction.cs @@ -0,0 +1,211 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.IO; +using Snap.Hutao.Core.LifeCycle; +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.Shell.Common; +using static Windows.Win32.PInvoke; + +namespace Snap.Hutao.Factory.Picker; + +[ConstructorGenerated] +[Injection(InjectAs.Transient, typeof(IFileSystemPickerInteraction))] +internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInteraction +{ + private readonly ICurrentWindowReference currentWindowReference; + + public unsafe ValueResult PickFile(string? title, string? defaultFileName, (string Name, string Type)[]? filters) + { + CoCreateInstance(default, CLSCTX.CLSCTX_INPROC_SERVER, out IFileOpenDialog dialog).ThrowOnFailure(); + + FILEOPENDIALOGOPTIONS options = + FILEOPENDIALOGOPTIONS.FOS_NOTESTFILECREATE | + FILEOPENDIALOGOPTIONS.FOS_FORCEFILESYSTEM | + FILEOPENDIALOGOPTIONS.FOS_NOCHANGEDIR; + + dialog.SetOptions(options); + SetDesktopAsStartupFolder(dialog); + + if (!string.IsNullOrEmpty(title)) + { + dialog.SetTitle(title); + } + + if (!string.IsNullOrEmpty(defaultFileName)) + { + dialog.SetFileName(defaultFileName); + } + + if (filters is { Length: > 0 }) + { + SetFileTypes(dialog, filters); + } + + HRESULT res = dialog.Show(currentWindowReference.GetWindowHandle()); + if (res == HRESULT_FROM_WIN32(WIN32_ERROR.ERROR_CANCELLED)) + { + return new(false, default); + } + else + { + Marshal.ThrowExceptionForHR(res); + } + + dialog.GetResult(out IShellItem item); + + PWSTR displayName = default; + string file; + try + { + item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName); + file = new((char*)displayName); + } + finally + { + Marshal.FreeCoTaskMem((nint)displayName.Value); + } + + return new(true, file); + } + + public unsafe ValueResult SaveFile(string? title, string? defaultFileName, (string Name, string Type)[]? filters) + { + CoCreateInstance(default, CLSCTX.CLSCTX_INPROC_SERVER, out IFileSaveDialog dialog).ThrowOnFailure(); + + FILEOPENDIALOGOPTIONS options = + FILEOPENDIALOGOPTIONS.FOS_NOTESTFILECREATE | + FILEOPENDIALOGOPTIONS.FOS_FORCEFILESYSTEM | + FILEOPENDIALOGOPTIONS.FOS_STRICTFILETYPES | + FILEOPENDIALOGOPTIONS.FOS_NOCHANGEDIR; + + dialog.SetOptions(options); + SetDesktopAsStartupFolder(dialog); + + if (!string.IsNullOrEmpty(title)) + { + dialog.SetTitle(title); + } + + if (!string.IsNullOrEmpty(defaultFileName)) + { + dialog.SetFileName(defaultFileName); + } + + if (filters is { Length: > 0 }) + { + SetFileTypes(dialog, filters); + } + + HRESULT res = dialog.Show(currentWindowReference.GetWindowHandle()); + if (res == HRESULT_FROM_WIN32(WIN32_ERROR.ERROR_CANCELLED)) + { + return new(false, default); + } + else + { + Marshal.ThrowExceptionForHR(res); + } + + dialog.GetResult(out IShellItem item); + + PWSTR displayName = default; + string file; + try + { + item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName); + file = new((char*)displayName); + } + finally + { + Marshal.FreeCoTaskMem((nint)displayName.Value); + } + + return new(true, file); + } + + public unsafe ValueResult PickFolder(string? title) + { + CoCreateInstance(default, CLSCTX.CLSCTX_INPROC_SERVER, out IFileOpenDialog dialog).ThrowOnFailure(); + + FILEOPENDIALOGOPTIONS options = + FILEOPENDIALOGOPTIONS.FOS_NOTESTFILECREATE | + FILEOPENDIALOGOPTIONS.FOS_FORCEFILESYSTEM | + FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS | + FILEOPENDIALOGOPTIONS.FOS_NOCHANGEDIR; + + dialog.SetOptions(options); + SetDesktopAsStartupFolder(dialog); + + if (!string.IsNullOrEmpty(title)) + { + dialog.SetTitle(title); + } + + HRESULT res = dialog.Show(currentWindowReference.GetWindowHandle()); + if (res == HRESULT_FROM_WIN32(WIN32_ERROR.ERROR_CANCELLED)) + { + return new(false, default!); + } + else + { + Marshal.ThrowExceptionForHR(res); + } + + dialog.GetResult(out IShellItem item); + + PWSTR displayName = default; + string file; + try + { + item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName); + file = new((char*)displayName); + } + finally + { + Marshal.FreeCoTaskMem((nint)displayName.Value); + } + + return new(true, file); + } + + private static unsafe void SetFileTypes(TDialog dialog, (string Name, string Type)[] filters) + where TDialog : IFileDialog + { + List unmanagedStringPtrs = new(filters.Length * 2); + List filterSpecs = new(filters.Length); + foreach ((string name, string type) in filters) + { + nint pName = Marshal.StringToHGlobalUni(name); + nint pType = Marshal.StringToHGlobalUni(type); + unmanagedStringPtrs.Add(pName); + unmanagedStringPtrs.Add(pType); + COMDLG_FILTERSPEC spec = default; + spec.pszName = *(PCWSTR*)&pName; + spec.pszSpec = *(PCWSTR*)&pType; + filterSpecs.Add(spec); + } + + fixed (COMDLG_FILTERSPEC* ptr = CollectionsMarshal.AsSpan(filterSpecs)) + { + dialog.SetFileTypes((uint)filterSpecs.Count, ptr); + } + + foreach (ref readonly nint ptr in CollectionsMarshal.AsSpan(unmanagedStringPtrs)) + { + Marshal.FreeHGlobal(ptr); + } + } + + private static unsafe void SetDesktopAsStartupFolder(TDialog dialog) + where TDialog : IFileDialog + { + string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); + SHCreateItemFromParsingName(desktopPath, default, typeof(IShellItem).GUID, out object shellItem).ThrowOnFailure(); + dialog.SetFolder((IShellItem)shellItem); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Picker/FileSystemPickerInteractionExtension.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Picker/FileSystemPickerInteractionExtension.cs new file mode 100644 index 00000000..62e7dd83 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Factory/Picker/FileSystemPickerInteractionExtension.cs @@ -0,0 +1,19 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.IO; + +namespace Snap.Hutao.Factory.Picker; + +internal static class FileSystemPickerInteractionExtension +{ + public static ValueResult PickFile(this IFileSystemPickerInteraction interaction, string? title, (string Name, string Type)[]? filters) + { + return interaction.PickFile(title, null, filters); + } + + public static ValueResult PickFolder(this IFileSystemPickerInteraction interaction) + { + return interaction.PickFolder(null); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Picker/IFileSystemPickerInteraction.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Picker/IFileSystemPickerInteraction.cs index bc085e6e..713d1dc3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/Picker/IFileSystemPickerInteraction.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/Picker/IFileSystemPickerInteraction.cs @@ -1,45 +1,15 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Core.LifeCycle; -using System.Runtime.InteropServices; -using Windows.Win32; -using Windows.Win32.Foundation; -using Windows.Win32.System.Com; -using Windows.Win32.UI.Shell; -using static Windows.Win32.PInvoke; +using Snap.Hutao.Core.IO; namespace Snap.Hutao.Factory.Picker; internal interface IFileSystemPickerInteraction { -} + ValueResult PickFile(string? title, string? defaultFileName, (string Name, string Type)[]? filters); -[ConstructorGenerated] -[Injection(InjectAs.Transient, typeof(IFileSystemPickerInteraction))] -internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInteraction -{ - private readonly ICurrentWindowReference currentWindowReference; + ValueResult PickFolder(string? title); - public unsafe string PickFile() - { - HRESULT result = CoCreateInstance(default, CLSCTX.CLSCTX_INPROC_SERVER, out IFileOpenDialog dialog); - Marshal.ThrowExceptionForHR(result); - - dialog.Show(currentWindowReference.GetWindowHandle()); - dialog.GetResult(out IShellItem item); - PWSTR name = default; - string file; - try - { - item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out name); - file = new((char*)name); - } - finally - { - Marshal.FreeCoTaskMem((nint)name.Value); - } - - return file; - } + ValueResult SaveFile(string? title, string? defaultFileName, (string Name, string Type)[]? filters); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Picker/IPickerFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Picker/IPickerFactory.cs deleted file mode 100644 index 353c6146..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Factory/Picker/IPickerFactory.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Windows.Storage.Pickers; - -namespace Snap.Hutao.Factory.Picker; - -/// -/// 文件选择器工厂 -/// -[HighQuality] -internal interface IPickerFactory -{ - /// - /// 获取 经过初始化的 - /// - /// 初始位置 - /// 提交按钮文本 - /// 文件类型 - /// 经过初始化的 - FileOpenPicker GetFileOpenPicker(PickerLocationId location, string commitButton, params string[] fileTypes); - - /// - /// 获取 经过初始化的 - /// - /// 初始位置 - /// 文件名 - /// 提交按钮文本 - /// 文件类型 - /// 经过初始化的 - FileSavePicker GetFileSavePicker(PickerLocationId location, string fileName, string commitButton, IDictionary> fileTypes); - - /// - /// 获取 经过初始化的 - /// - /// 经过初始化的 - FolderPicker GetFolderPicker(); -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Picker/PickerFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Picker/PickerFactory.cs deleted file mode 100644 index 6c4ced9e..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Factory/Picker/PickerFactory.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Core; -using Snap.Hutao.Core.LifeCycle; -using Windows.Storage.Pickers; -using Windows.Win32.Foundation; -using WinRT.Interop; - -namespace Snap.Hutao.Factory.Picker; - -/// -[HighQuality] -[ConstructorGenerated] -[Injection(InjectAs.Transient, typeof(IPickerFactory))] -internal sealed partial class PickerFactory : IPickerFactory -{ - private const string AnyType = "*"; - - private readonly ICurrentWindowReference currentWindowReference; - - /// - public FileOpenPicker GetFileOpenPicker(PickerLocationId location, string commitButton, params string[] fileTypes) - { - FileOpenPicker picker = GetInitializedPicker(); - - picker.SuggestedStartLocation = location; - picker.CommitButtonText = commitButton; - - foreach (string type in fileTypes) - { - picker.FileTypeFilter.Add(type); - } - - // below Windows 11 - if (!UniversalApiContract.IsPresent(WindowsVersion.Windows11)) - { - // https://github.com/microsoft/WindowsAppSDK/issues/2931 - picker.FileTypeFilter.Add(AnyType); - } - - return picker; - } - - /// - public FileSavePicker GetFileSavePicker(PickerLocationId location, string fileName, string commitButton, IDictionary> fileTypes) - { - FileSavePicker picker = GetInitializedPicker(); - - picker.SuggestedStartLocation = location; - picker.SuggestedFileName = fileName; - picker.CommitButtonText = commitButton; - - foreach (KeyValuePair> kvp in fileTypes) - { - picker.FileTypeChoices.Add(kvp); - } - - return picker; - } - - /// - public FolderPicker GetFolderPicker() - { - FolderPicker picker = GetInitializedPicker(); - - // below Windows 11 - if (!UniversalApiContract.IsPresent(WindowsVersion.Windows11)) - { - // https://github.com/microsoft/WindowsAppSDK/issues/2931 - picker.FileTypeFilter.Add(AnyType); - } - - return picker; - } - - private T GetInitializedPicker() - where T : new() - { - // Create a folder picker. - T picker = new(); - - HWND hwnd = currentWindowReference.GetWindowHandle(); - InitializeWithWindow.Initialize(picker, hwnd); - - return picker; - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs index 52532905..85e8f047 100644 --- a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs @@ -16,8 +16,8 @@ namespace Snap.Hutao; [SuppressMessage("", "CA1001")] internal sealed partial class MainWindow : Window, IWindowOptionsSource, IMinMaxInfoHandler { - private const int MinWidth = 848; - private const int MinHeight = 524; + private const int MinWidth = 1200; + private const int MinHeight = 750; private readonly WindowOptions windowOptions; private readonly ILogger logger; diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest index 3a3dac4c..4fc3eba6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest +++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest @@ -12,7 +12,7 @@ + Version="1.7.19.0" /> Snap Hutao diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 882f607f..d2afe321 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -599,6 +599,12 @@ 新增:{0} 个成就 | 更新:{1} 个成就 | 删除:{2} 个成就 + + UIAF Json 文件 + + + 打开 UIAF Json 文件 + 单个成就存档内发现多个相同的成就 Id @@ -869,6 +875,9 @@ 选择游戏本体 + + 游戏本体 + 找不到 Unity 日志文件 @@ -1334,6 +1343,9 @@ 确定要删除存档 {0} 吗? + + 导出 UIAF Json 文件到指定路径 + 获取培养材料中,请稍候... @@ -1460,9 +1472,15 @@ 从胡桃云服务同步祈愿记录 + + 导出 UIGF Json 文件到指定路径 + 正在上传到胡桃云服务 + + 导入 UIGF Json 文件 + 我已阅读并同意上方的条款 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/GameRecordCharacterAvatarInfoTransformer.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/GameRecordCharacterAvatarInfoTransformer.cs index cd6cfeff..141e0ae4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/GameRecordCharacterAvatarInfoTransformer.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Transformer/GameRecordCharacterAvatarInfoTransformer.cs @@ -44,21 +44,18 @@ internal sealed class GameRecordCharacterAvatarInfoTransformer : IAvatarInfoTran Equip equip = avatarInfo.EquipList.Last(); - if (equip.ItemId != source.Weapon.Id) + // 切换了武器 + equip.ItemId = source.Weapon.Id; + equip.Weapon = new() { - // 切换了武器 - equip.ItemId = source.Weapon.Id; - equip.Weapon = new() + Level = source.Weapon.Level, + AffixMap = new() { - Level = source.Weapon.Level, - AffixMap = new() - { - [100000 + source.Weapon.Id] = source.Weapon.AffixLevel - 1, - }, - }; + [100000U + source.Weapon.Id] = source.Weapon.AffixLevel - 1, + }, + }; - // Special case here, don't set EQUIP_WEAPON - equip.Flat = new() { ItemType = ItemType.ITEM_WEAPON, }; - } + // Special case here, don't set EQUIP_WEAPON + equip.Flat = new() { ItemType = ItemType.ITEM_WEAPON, }; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs index 8e8ec8db..554970a2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs @@ -3,7 +3,6 @@ using Snap.Hutao.Core.IO; using Snap.Hutao.Factory.Picker; -using Windows.Storage.Pickers; namespace Snap.Hutao.Service.Game.Locator; @@ -15,30 +14,24 @@ namespace Snap.Hutao.Service.Game.Locator; [Injection(InjectAs.Transient)] internal sealed partial class ManualGameLocator : IGameLocator { - private readonly ITaskContext taskContext; - private readonly IPickerFactory pickerFactory; + private readonly IFileSystemPickerInteraction fileSystemPickerInteraction; /// - public async ValueTask> LocateGamePathAsync() + public ValueTask> LocateGamePathAsync() { - await taskContext.SwitchToMainThreadAsync(); - - FileOpenPicker picker = pickerFactory.GetFileOpenPicker( - PickerLocationId.Desktop, + (bool isPickerOk, ValueFile file) = fileSystemPickerInteraction.PickFile( SH.ServiceGameLocatorFileOpenPickerCommitText, - ".exe"); - - (bool isPickerOk, ValueFile file) = await picker.TryPickSingleFileAsync().ConfigureAwait(false); + [(SH.ServiceGameLocatorPickerFilterText, $"{GameConstants.YuanShenFileName};{GameConstants.GenshinImpactFileName}")]); if (isPickerOk) { string fileName = System.IO.Path.GetFileName(file); if (fileName is GameConstants.YuanShenFileName or GameConstants.GenshinImpactFileName) { - return new(true, file); + return ValueTask.FromResult>(new(true, file)); } } - return new(false, default!); + return ValueTask.FromResult>(new(false, default!)); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 164c6fd3..a7996319 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -11,7 +11,6 @@ true win-x64 win-x64 - win10-$(Platform).pubxml true False False @@ -89,6 +88,7 @@ + @@ -282,6 +282,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -316,6 +317,12 @@ + + + MSBuild:Compile + + + MSBuild:Compile diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml index cee08806..3de375ba 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml @@ -121,7 +121,10 @@ - + + TextTrimming="CharacterEllipsis" + TextWrapping="NoWrap"/> - - - - - - - + + + - - - + + + - + - + ColumnSpacing="8" + Columns="2" + RowSpacing="8"> - + + - - - + + + + - - - - - + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs index a2937b94..8cd1a283 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementImporter.cs @@ -11,7 +11,6 @@ using Snap.Hutao.Model.InterChange.Achievement; using Snap.Hutao.Service.Achievement; using Snap.Hutao.Service.Notification; using Snap.Hutao.View.Dialog; -using Windows.Storage.Pickers; using EntityAchievementArchive = Snap.Hutao.Model.Entity.AchievementArchive; namespace Snap.Hutao.ViewModel.Achievement; @@ -24,11 +23,11 @@ namespace Snap.Hutao.ViewModel.Achievement; [Injection(InjectAs.Scoped)] internal sealed partial class AchievementImporter { + private readonly IFileSystemPickerInteraction fileSystemPickerInteraction; private readonly IContentDialogFactory contentDialogFactory; private readonly IAchievementService achievementService; private readonly IClipboardProvider clipboardInterop; private readonly IInfoBarService infoBarService; - private readonly IPickerFactory pickerFactory; private readonly JsonSerializerOptions options; private readonly ITaskContext taskContext; @@ -65,10 +64,9 @@ internal sealed partial class AchievementImporter { if (achievementService.CurrentArchive is { } archive) { - ValueResult pickerResult = await pickerFactory - .GetFileOpenPicker(PickerLocationId.Desktop, SH.FilePickerImportCommit, ".json") - .TryPickSingleFileAsync() - .ConfigureAwait(false); + ValueResult pickerResult = fileSystemPickerInteraction.PickFile( + SH.ServiceAchievementUIAFImportPickerTitile, + [(SH.ServiceAchievementUIAFImportPickerFilterText, ".json")]); if (pickerResult.TryGetValue(out ValueFile file)) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs index f4fd36c3..f94970de 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs @@ -15,7 +15,6 @@ using Snap.Hutao.Service.Notification; using Snap.Hutao.View.Dialog; using System.Collections.ObjectModel; using System.Text.RegularExpressions; -using Windows.Storage.Pickers; using EntityAchievementArchive = Snap.Hutao.Model.Entity.AchievementArchive; using MetadataAchievement = Snap.Hutao.Model.Metadata.Achievement.Achievement; using MetadataAchievementGoal = Snap.Hutao.Model.Metadata.Achievement.AchievementGoal; @@ -33,8 +32,8 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav private readonly SortDescription uncompletedItemsFirstSortDescription = new(nameof(AchievementView.IsChecked), SortDirection.Ascending); private readonly SortDescription completionTimeSortDescription = new(nameof(AchievementView.Time), SortDirection.Descending); + private readonly IFileSystemPickerInteraction fileSystemPickerInteraction; private readonly IContentDialogFactory contentDialogFactory; - private readonly IPickerFactory pickerFactory; private readonly AchievementImporter achievementImporter; private readonly IAchievementService achievementService; private readonly IMetadataService metadataService; @@ -257,17 +256,12 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav { if (SelectedArchive is not null && Achievements is not null) { - string fileName = $"{achievementService.CurrentArchive?.Name}.json"; - Dictionary> fileTypes = new() - { - [SH.ViewModelAchievementExportFileType] = [".json"], - }; + (bool isOk, ValueFile file) = fileSystemPickerInteraction.SaveFile( + SH.ViewModelAchievementUIAFExportPickerTitle, + $"{achievementService.CurrentArchive?.Name}.json", + [(SH.ViewModelAchievementExportFileType, "*.json")]); - FileSavePicker picker = pickerFactory - .GetFileSavePicker(PickerLocationId.Desktop, fileName, SH.FilePickerExportCommit, fileTypes); - - (bool isPickerOk, ValueFile file) = await picker.TryPickSaveFileAsync().ConfigureAwait(false); - if (isPickerOk) + if (isOk) { UIAF uiaf = await achievementService.ExportToUIAFAsync(SelectedArchive).ConfigureAwait(false); if (await file.SerializeToJsonAsync(uiaf, options).ConfigureAwait(false)) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs index 478ba2e7..6d32962b 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs @@ -17,7 +17,6 @@ using Snap.Hutao.Service.Notification; using Snap.Hutao.View.Dialog; using System.Collections.ObjectModel; using System.Runtime.InteropServices; -using Windows.Storage.Pickers; namespace Snap.Hutao.ViewModel.GachaLog; @@ -31,13 +30,13 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel { private readonly HutaoCloudStatisticsViewModel hutaoCloudStatisticsViewModel; private readonly IGachaLogQueryProviderFactory gachaLogQueryProviderFactory; + private readonly IFileSystemPickerInteraction fileSystemPickerInteraction; private readonly IContentDialogFactory contentDialogFactory; private readonly HutaoCloudViewModel hutaoCloudViewModel; private readonly IProgressFactory progressFactory; private readonly IGachaLogService gachaLogService; private readonly IInfoBarService infoBarService; private readonly JsonSerializerOptions options; - private readonly IPickerFactory pickerFactory; private readonly ITaskContext taskContext; private ObservableCollection? archives; @@ -215,52 +214,52 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel [Command("ImportFromUIGFJsonCommand")] private async Task ImportFromUIGFJsonAsync() { - FileOpenPicker picker = pickerFactory.GetFileOpenPicker(PickerLocationId.Desktop, SH.FilePickerImportCommit, ".json"); - (bool isPickerOk, ValueFile file) = await picker.TryPickSingleFileAsync().ConfigureAwait(false); - if (isPickerOk) + (bool isOk, ValueFile file) = fileSystemPickerInteraction.PickFile( + SH.ViewModelGachaUIGFImportPickerTitile, + [(SH.ViewModelGachaLogExportFileType, "*.json")]); + + if (!isOk) { - ValueResult result = await file.DeserializeFromJsonAsync(options).ConfigureAwait(false); - if (result.TryGetValue(out UIGF? uigf)) - { - await TryImportUIGFInternalAsync(uigf).ConfigureAwait(false); - } - else - { - infoBarService.Error(SH.ViewModelImportWarningTitle, SH.ViewModelImportWarningMessage); - } + return; + } + + ValueResult result = await file.DeserializeFromJsonAsync(options).ConfigureAwait(false); + if (result.TryGetValue(out UIGF? uigf)) + { + await TryImportUIGFInternalAsync(uigf).ConfigureAwait(false); + } + else + { + infoBarService.Error(SH.ViewModelImportWarningTitle, SH.ViewModelImportWarningMessage); } } [Command("ExportToUIGFJsonCommand")] private async Task ExportToUIGFJsonAsync() { - if (SelectedArchive is not null) + if (SelectedArchive is null) { - Dictionary> fileTypes = new() - { - [SH.ViewModelGachaLogExportFileType] = [".json"], - }; + return; + } - FileSavePicker picker = pickerFactory.GetFileSavePicker( - PickerLocationId.Desktop, - $"{SelectedArchive.Uid}.json", - SH.FilePickerExportCommit, - fileTypes); + (bool isOk, ValueFile file) = fileSystemPickerInteraction.SaveFile( + SH.ViewModelGachaLogUIGFExportPickerTitle, + $"{SelectedArchive.Uid}.json", + [(SH.ViewModelGachaLogExportFileType, "*.json")]); - (bool isPickerOk, ValueFile file) = await picker.TryPickSaveFileAsync().ConfigureAwait(false); + if (!isOk) + { + return; + } - if (isPickerOk) - { - UIGF uigf = await gachaLogService.ExportToUIGFAsync(SelectedArchive).ConfigureAwait(false); - if (await file.SerializeToJsonAsync(uigf, options).ConfigureAwait(false)) - { - infoBarService.Success(SH.ViewModelExportSuccessTitle, SH.ViewModelExportSuccessMessage); - } - else - { - infoBarService.Warning(SH.ViewModelExportWarningTitle, SH.ViewModelExportWarningMessage); - } - } + UIGF uigf = await gachaLogService.ExportToUIGFAsync(SelectedArchive).ConfigureAwait(false); + if (await file.SerializeToJsonAsync(uigf, options).ConfigureAwait(false)) + { + infoBarService.Success(SH.ViewModelExportSuccessTitle, SH.ViewModelExportSuccessMessage); + } + else + { + infoBarService.Warning(SH.ViewModelExportWarningTitle, SH.ViewModelExportWarningMessage); } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs index 57ffa664..9aec077f 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs @@ -26,7 +26,6 @@ using Snap.Hutao.ViewModel.Guide; using System.Globalization; using System.IO; using System.Runtime.InteropServices; -using Windows.Storage.Pickers; using Windows.System; namespace Snap.Hutao.ViewModel.Setting; @@ -41,6 +40,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel { private readonly HomeCardOptions homeCardOptions = new(); + private readonly IFileSystemPickerInteraction fileSystemPickerInteraction; private readonly HutaoPassportViewModel hutaoPassportViewModel; private readonly IContentDialogFactory contentDialogFactory; private readonly IGameLocatorFactory gameLocatorFactory; @@ -50,7 +50,6 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel private readonly HutaoUserOptions hutaoUserOptions; private readonly IInfoBarService infoBarService; private readonly RuntimeOptions runtimeOptions; - private readonly IPickerFactory pickerFactory; private readonly HotKeyOptions hotKeyOptions; private readonly IUserService userService; private readonly ITaskContext taskContext; @@ -143,8 +142,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel [Command("SetPowerShellPathCommand")] private async Task SetPowerShellPathAsync() { - FileOpenPicker picker = pickerFactory.GetFileOpenPicker(PickerLocationId.DocumentsLibrary, SH.FilePickerPowerShellCommit, ".exe"); - (bool isOk, ValueFile file) = await picker.TryPickSingleFileAsync().ConfigureAwait(false); + (bool isOk, ValueFile file) = fileSystemPickerInteraction.PickFile(SH.FilePickerPowerShellCommit, [("PowerShell", "powershell.exe")]); if (isOk && Path.GetFileNameWithoutExtension(file).Equals("POWERSHELL", StringComparison.OrdinalIgnoreCase)) { @@ -200,12 +198,9 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel } [Command("SetDataFolderCommand")] - private async Task SetDataFolderAsync() + private void SetDataFolder() { - (bool isOk, string folder) = await pickerFactory - .GetFolderPicker() - .TryPickSingleFolderAsync() - .ConfigureAwait(false); + (bool isOk, string folder) = fileSystemPickerInteraction.PickFolder(); if (isOk) {