mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
refactor user infrastructure
This commit is contained in:
@@ -35,13 +35,14 @@ public class CheckBoxWithDescriptionControl : CheckBox
|
||||
|
||||
private void CheckBoxSubTextControl_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
StackPanel panel = new StackPanel() { Orientation = Orientation.Vertical };
|
||||
StackPanel panel = new() { Orientation = Orientation.Vertical };
|
||||
|
||||
// Add text box only if the description is not empty. Required for additional plugin options.
|
||||
if (!string.IsNullOrWhiteSpace(Description))
|
||||
{
|
||||
panel.Children.Add(new TextBlock() { Margin = new Thickness(0, 10, 0, 0), Text = Header });
|
||||
panel.Children.Add(new IsEnabledTextBlock() { Style = (Style)Application.Current.Resources["SecondaryIsEnabledTextBlockStyle"], Text = Description });
|
||||
//Style = (Style)Application.Current.Resources["SecondaryIsEnabledTextBlockStyle"]
|
||||
panel.Children.Add(new IsEnabledTextBlock() { Text = Description });
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<Style x:Key="SettingButtonStyle" TargetType="Button" BasedOn="{StaticResource DefaultButtonStyle}" >
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}" />
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
|
||||
<Setter Property="Padding" Value="16,0,16,0" />
|
||||
<Setter Property="Padding" Value="16,4,16,4" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="HyperlinkButtonStyle" TargetType="HyperlinkButton" >
|
||||
|
||||
@@ -28,8 +28,4 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="CoreEnvironment\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -28,6 +28,7 @@ public partial class App : Application
|
||||
/// </summary>
|
||||
public App()
|
||||
{
|
||||
AppInstance.GetCurrent().Activated += OnActivated;
|
||||
// load app resource
|
||||
InitializeComponent();
|
||||
InitializeDependencyInjection();
|
||||
@@ -52,16 +53,24 @@ public partial class App : Application
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="Windows.Storage.ApplicationData.Current"/>
|
||||
/// <inheritdoc cref="ApplicationData.Current.TemporaryFolder"/>
|
||||
/// </summary>
|
||||
[SuppressMessage("", "CA1822")]
|
||||
public StorageFolder CacheFolder
|
||||
public static StorageFolder CacheFolder
|
||||
{
|
||||
get => ApplicationData.Current.TemporaryFolder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="ApplicationData.Current.LocalSettings"/>
|
||||
/// </summary>
|
||||
public static ApplicationDataContainer Settings
|
||||
{
|
||||
get => ApplicationData.Current.LocalSettings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the application is launched.
|
||||
/// Any async operation in this method should be wrapped with try catch
|
||||
/// </summary>
|
||||
/// <param name="args">Details about the launch request and process.</param>
|
||||
[SuppressMessage("", "VSTHRD100")]
|
||||
@@ -69,7 +78,6 @@ public partial class App : Application
|
||||
{
|
||||
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
AppInstance firstInstance = AppInstance.FindOrRegisterForKey("main");
|
||||
firstInstance.Activated += OnActivated;
|
||||
|
||||
if (!firstInstance.IsCurrent)
|
||||
{
|
||||
@@ -83,7 +91,6 @@ public partial class App : Application
|
||||
Window.Activate();
|
||||
|
||||
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", CacheFolder.Path);
|
||||
logger.LogInformation(EventIds.CommonLog, "Data folder : {folder}", CacheFolder.Path);
|
||||
|
||||
Ioc.Default
|
||||
.GetRequiredService<IMetadataService>()
|
||||
@@ -117,19 +124,22 @@ public partial class App : Application
|
||||
Ioc.Default.ConfigureServices(services);
|
||||
}
|
||||
|
||||
private void AppUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");
|
||||
}
|
||||
|
||||
private void OnActivated(object? sender, AppActivationArguments args)
|
||||
{
|
||||
if (args.TryGetProtocolActivatedUri(out Uri? uri))
|
||||
if (args.Kind == ExtendedActivationKind.Protocol)
|
||||
{
|
||||
Ioc.Default.GetRequiredService<IInfoBarService>().Information(uri.ToString());
|
||||
if (args.TryGetProtocolActivatedUri(out Uri? uri))
|
||||
{
|
||||
Ioc.Default.GetRequiredService<IInfoBarService>().Information(uri.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AppUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常: [HResult:{code}]", e.Exception.HResult);
|
||||
}
|
||||
|
||||
private void XamlBindingFailed(object sender, BindingFailedEventArgs e)
|
||||
{
|
||||
logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message);
|
||||
|
||||
@@ -5,7 +5,9 @@ using CommunityToolkit.WinUI.UI.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Core.Exception;
|
||||
using Snap.Hutao.Extension;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Control.Image;
|
||||
@@ -25,30 +27,34 @@ public class CachedImage : ImageEx
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<ImageSource> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
||||
protected override async Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
||||
{
|
||||
IImageCache imageCache = Ioc.Default.GetRequiredService<IImageCache>();
|
||||
|
||||
try
|
||||
{
|
||||
Verify.Operation(imageUri.Host != string.Empty, "可能是空绑定产生的 [ms-appx:///]");
|
||||
Verify.Operation(imageUri.Host != string.Empty, "无效的Uri");
|
||||
StorageFile file = await imageCache.GetFileFromCacheAsync(imageUri);
|
||||
|
||||
// check token state to determine whether the operation should be canceled.
|
||||
Must.TryThrowOnCanceled(token, "Image source has changed.");
|
||||
Must.ThrowOnCanceled(token, "Image source has changed.");
|
||||
|
||||
// return a BitmapImage initialize with a uri will increase image quality.
|
||||
// BitmapImage initialize with a uri will increase image quality.
|
||||
return new BitmapImage(new(file.Path));
|
||||
}
|
||||
catch (COMException ex) when (ex.Is(COMError.WINCODEC_ERR_COMPONENTNOTFOUND))
|
||||
{
|
||||
// The image is corrupted, remove it.
|
||||
await imageCache.RemoveAsync(imageUri.Enumerate()).ConfigureAwait(false);
|
||||
return null;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// task was explicitly canceled
|
||||
throw;
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// maybe the image is corrupted, remove it.
|
||||
await imageCache.RemoveAsync(imageUri.Enumerate()).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using Microsoft.UI.Xaml.Hosting;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Core.Exception;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
@@ -72,9 +73,8 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
return LoadedImageSurface.StartLoadFromStream(imageStream);
|
||||
}
|
||||
}
|
||||
catch (COMException ex) when (ex.HResult == unchecked((int)0x88982F50))
|
||||
catch (COMException ex) when (ex.Is(COMError.WINCODEC_ERR_COMPONENTNOTFOUND))
|
||||
{
|
||||
// COMException (0x88982F50): 无法找不到组件。
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 封装了打开浏览器的方法
|
||||
/// </summary>
|
||||
public static class Browser
|
||||
{
|
||||
/// <summary>
|
||||
/// 打开浏览器
|
||||
/// </summary>
|
||||
/// <param name="url">链接</param>
|
||||
/// <param name="failAction">失败时执行的回调</param>
|
||||
public static void Open(string url, Action<Exception>? failAction = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessHelper.Start(url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failAction?.Invoke(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,7 +216,7 @@ public abstract class CacheBase<T>
|
||||
|
||||
using (await cacheFolderSemaphore.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
baseFolder ??= App.Current.CacheFolder;
|
||||
baseFolder ??= App.CacheFolder;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFolderName))
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Core;
|
||||
/// <summary>
|
||||
/// 核心环境参数
|
||||
/// </summary>
|
||||
internal static partial class CoreEnvironment
|
||||
internal static class CoreEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前版本
|
||||
@@ -42,9 +42,4 @@ internal static partial class CoreEnvironment
|
||||
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
|
||||
return Md5Convert.ToHexString($"{userName}{machineGuid}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static partial class CoreEnvironment
|
||||
{
|
||||
|
||||
}
|
||||
15
src/Snap.Hutao/Snap.Hutao/Core/Exception/COMError.cs
Normal file
15
src/Snap.Hutao/Snap.Hutao/Core/Exception/COMError.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Exception;
|
||||
|
||||
/// <summary>
|
||||
/// Error codes used by COM-based APIs.
|
||||
/// </summary>
|
||||
public enum COMError : uint
|
||||
{
|
||||
/// <summary>
|
||||
/// The component cannot be found.
|
||||
/// </summary>
|
||||
WINCODEC_ERR_COMPONENTNOTFOUND = 0x88982F50,
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Core.Exception;
|
||||
|
||||
/// <summary>
|
||||
/// COM异常扩展
|
||||
/// </summary>
|
||||
internal static class COMExceptionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 比较COM异常是否与某个错误代码等价
|
||||
/// </summary>
|
||||
/// <param name="exception">异常</param>
|
||||
/// <param name="code">错误代码</param>
|
||||
/// <returns>是否为该错误</returns>
|
||||
public static bool Is(this COMException exception, COMError code)
|
||||
{
|
||||
return exception.HResult == unchecked((int)code);
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ internal sealed partial class DatebaseLogger : ILogger
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, System.Exception? exception, Func<TState, System.Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel))
|
||||
{
|
||||
|
||||
@@ -14,13 +14,12 @@ internal static class LocalSetting
|
||||
/// <summary>
|
||||
/// 由于 <see cref="Windows.Foundation.Collections.IPropertySet"/> 没有 nullable context,
|
||||
/// 在处理引用类型时需要格外小心
|
||||
/// 将值类型的操作与引用类型区分开,可以提升一定的性能
|
||||
/// </summary>
|
||||
private static readonly ApplicationDataContainer Container;
|
||||
|
||||
static LocalSetting()
|
||||
{
|
||||
Container = ApplicationData.Current.LocalSettings;
|
||||
Container = App.Settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -79,17 +78,11 @@ internal static class LocalSetting
|
||||
/// <typeparam name="T">设置项的类型</typeparam>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">值</param>
|
||||
public static void Set<T>(string key, T? value)
|
||||
where T : class
|
||||
/// <returns>设置的值</returns>
|
||||
public static T Set<T>(string key, T value)
|
||||
{
|
||||
try
|
||||
{
|
||||
Container.Values[key] = value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
}
|
||||
Container.Values[key] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -105,7 +98,7 @@ internal static class LocalSetting
|
||||
{
|
||||
Container.Values[key] = value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Runtime;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.Validation;
|
||||
@@ -20,42 +18,20 @@ public static class Must
|
||||
/// <param name="parameterName">The name of the parameter to include in any thrown exception.</param>
|
||||
/// <returns>The value of the parameter.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is <c>null</c>.</exception>
|
||||
[DebuggerStepThrough]
|
||||
[TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
|
||||
public static T NotNull<T>([NotNull] T value, [CallerArgumentExpression("value")] string? parameterName = null)
|
||||
where T : class // ensures value-types aren't passed to a null checking method
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
throw new ArgumentNullException(parameterName);
|
||||
}
|
||||
|
||||
return value;
|
||||
return value ?? throw new ArgumentNullException(parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unconditionally throws an <see cref="NotSupportedException"/>.
|
||||
/// </summary>
|
||||
/// <returns>Nothing. This method always throws.</returns>
|
||||
[DebuggerStepThrough]
|
||||
[DoesNotReturn]
|
||||
public static Exception NeverHappen()
|
||||
public static System.Exception NeverHappen()
|
||||
{
|
||||
// Keep these two as separate lines of code, so the debugger can come in during the assert dialog
|
||||
// that the exception's constructor displays, and the debugger can then be made to skip the throw
|
||||
// in order to continue the investigation.
|
||||
NotSupportedException exception = new("该行为不应发生,请联系开发者进一步确认");
|
||||
bool proceed = true; // allows debuggers to skip the throw statement
|
||||
if (proceed)
|
||||
{
|
||||
throw exception;
|
||||
}
|
||||
else
|
||||
{
|
||||
#pragma warning disable CS8763
|
||||
return new Exception();
|
||||
#pragma warning restore CS8763
|
||||
}
|
||||
throw new NotSupportedException("该行为不应发生,请联系开发者进一步确认");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -63,26 +39,11 @@ public static class Must
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type that the method should be typed to return (although it never returns anything).</typeparam>
|
||||
/// <returns>Nothing. This method always throws.</returns>
|
||||
[DebuggerStepThrough]
|
||||
[DoesNotReturn]
|
||||
[return: MaybeNull]
|
||||
public static T NeverHappen<T>()
|
||||
{
|
||||
// Keep these two as separate lines of code, so the debugger can come in during the assert dialog
|
||||
// that the exception's constructor displays, and the debugger can then be made to skip the throw
|
||||
// in order to continue the investigation.
|
||||
NotSupportedException exception = new("该行为不应发生,请联系开发者进一步确认");
|
||||
bool proceed = true; // allows debuggers to skip the throw statement
|
||||
if (proceed)
|
||||
{
|
||||
throw exception;
|
||||
}
|
||||
else
|
||||
{
|
||||
#pragma warning disable CS8763 // 不应返回标记为 [DoesNotReturn] 的方法。
|
||||
return default;
|
||||
#pragma warning restore CS8763 // 不应返回标记为 [DoesNotReturn] 的方法。
|
||||
}
|
||||
throw new NotSupportedException("该行为不应发生,请联系开发者进一步确认");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -91,9 +52,8 @@ public static class Must
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <param name="message">取消消息</param>
|
||||
/// <exception cref="TaskCanceledException">任务被取消</exception>
|
||||
[DebuggerStepThrough]
|
||||
[SuppressMessage("", "CA1068")]
|
||||
public static void TryThrowOnCanceled(CancellationToken token, string message)
|
||||
public static void ThrowOnCanceled(CancellationToken token, string message)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
@@ -35,7 +36,7 @@ internal abstract class WebView2Helper
|
||||
{
|
||||
version = CoreWebView2Environment.GetAvailableBrowserVersionString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
ILogger<WebView2Helper> logger = Ioc.Default.GetRequiredService<ILogger<WebView2Helper>>();
|
||||
logger.LogError(EventIds.WebView2EnvironmentException, ex, "WebView2 运行时未安装");
|
||||
|
||||
82
src/Snap.Hutao/Snap.Hutao/Core/WindowManager.cs
Normal file
82
src/Snap.Hutao/Snap.Hutao/Core/WindowManager.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Control.HostBackdrop;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Core.Win32;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 窗口状态管理器
|
||||
/// </summary>
|
||||
internal class WindowManager
|
||||
{
|
||||
private readonly IntPtr handle;
|
||||
private readonly Window window;
|
||||
private readonly UIElement titleBar;
|
||||
private readonly ILogger<WindowManager> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的窗口状态管理器
|
||||
/// </summary>
|
||||
/// <param name="window">窗口</param>
|
||||
/// <param name="titleBar">充当标题栏的元素</param>
|
||||
public WindowManager(Window window, UIElement titleBar)
|
||||
{
|
||||
this.window = window;
|
||||
this.titleBar = titleBar;
|
||||
logger = Ioc.Default.GetRequiredService<ILogger<WindowManager>>();
|
||||
|
||||
handle = WindowNative.GetWindowHandle(window);
|
||||
|
||||
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 static void SaveWindowRect(IntPtr handle)
|
||||
{
|
||||
WINDOWPLACEMENT windowPlacement = WINDOWPLACEMENT.Default;
|
||||
User32.GetWindowPlacement(handle, ref windowPlacement);
|
||||
|
||||
LocalSetting.Set(SettingKeys.WindowLeft, windowPlacement.NormalPosition.Left);
|
||||
LocalSetting.Set(SettingKeys.WindowTop, windowPlacement.NormalPosition.Top);
|
||||
LocalSetting.Set(SettingKeys.WindowRight, windowPlacement.NormalPosition.Right);
|
||||
LocalSetting.Set(SettingKeys.WindowBottom, windowPlacement.NormalPosition.Bottom);
|
||||
}
|
||||
|
||||
private void InitializeWindow()
|
||||
{
|
||||
window.ExtendsContentIntoTitleBar = true;
|
||||
window.SetTitleBar(titleBar);
|
||||
window.Closed += OnWindowClosed;
|
||||
|
||||
User32.SetWindowText(handle, "胡桃");
|
||||
RECT rect = RetriveWindowRect();
|
||||
if (rect.Area > 0)
|
||||
{
|
||||
WINDOWPLACEMENT windowPlacement = WINDOWPLACEMENT.Create(new POINT(-1, -1), rect, ShowWindowCommand.Normal);
|
||||
User32.SetWindowPlacement(handle, ref windowPlacement);
|
||||
}
|
||||
|
||||
bool micaApplied = new SystemBackdrop(window).TrySetBackdrop();
|
||||
logger.LogInformation(EventIds.BackdropState, "Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed");
|
||||
}
|
||||
|
||||
private void OnWindowClosed(object sender, WindowEventArgs args)
|
||||
{
|
||||
SaveWindowRect(handle);
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,8 @@ public static class AppActivationArgumentsExtensions
|
||||
public static bool TryGetProtocolActivatedUri(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out Uri? uri)
|
||||
{
|
||||
uri = null;
|
||||
if (activatedEventArgs.Kind == ExtendedActivationKind.Protocol)
|
||||
if (activatedEventArgs.Data is IProtocolActivatedEventArgs protocolArgs)
|
||||
{
|
||||
IProtocolActivatedEventArgs protocolArgs = (activatedEventArgs.Data as IProtocolActivatedEventArgs)!;
|
||||
uri = protocolArgs.Uri;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -19,5 +19,4 @@ public static class PackageVersionExtensions
|
||||
{
|
||||
return new Version(packageVersion.Major, packageVersion.Minor, packageVersion.Build, packageVersion.Revision);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Snap.Hutao;
|
||||
/// </summary>
|
||||
internal static class IocHttpClientConfiguration
|
||||
{
|
||||
private const string CommonUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Snap Hutao";
|
||||
private static readonly string CommonUA = $"Snap Hutao/{Core.CoreEnvironment.Version}";
|
||||
|
||||
/// <summary>
|
||||
/// 添加 <see cref="HttpClient"/>
|
||||
@@ -30,7 +30,8 @@ internal static class IocHttpClientConfiguration
|
||||
{
|
||||
// services
|
||||
services.AddHttpClient<MetadataService>(DefaultConfiguration);
|
||||
services.AddHttpClient<ImageCache>(DefaultConfiguration).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { MaxConnectionsPerServer = 20 });
|
||||
services.AddHttpClient<ImageCache>(DefaultConfiguration)
|
||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { MaxConnectionsPerServer = 20 });
|
||||
|
||||
// normal clients
|
||||
services.AddHttpClient<AnnouncementClient>(DefaultConfiguration);
|
||||
|
||||
@@ -3,11 +3,7 @@
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Context.Database;
|
||||
using Snap.Hutao.Control.HostBackdrop;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Core.Win32;
|
||||
using WinRT.Interop;
|
||||
using Snap.Hutao.Core;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
@@ -18,8 +14,6 @@ namespace Snap.Hutao;
|
||||
public sealed partial class MainWindow : Window
|
||||
{
|
||||
private readonly AppDbContext appDbContext;
|
||||
private readonly ILogger<MainWindow> logger;
|
||||
private readonly IntPtr handle;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的主窗体
|
||||
@@ -29,56 +23,13 @@ public sealed partial class MainWindow : Window
|
||||
public MainWindow(AppDbContext appDbContext, ILogger<MainWindow> logger)
|
||||
{
|
||||
this.appDbContext = appDbContext;
|
||||
this.logger = logger;
|
||||
|
||||
InitializeComponent();
|
||||
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 static void SaveWindowRect(IntPtr handle)
|
||||
{
|
||||
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 InitializeWindow()
|
||||
{
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
SetTitleBar(TitleBarView.DragableArea);
|
||||
|
||||
User32.SetWindowText(handle, "胡桃");
|
||||
RECT rect = RetriveWindowRect();
|
||||
if (rect.Area > 0)
|
||||
{
|
||||
WINDOWPLACEMENT windowPlacement = WINDOWPLACEMENT.Create(new POINT(-1, -1), rect, ShowWindowCommand.Normal);
|
||||
User32.SetWindowPlacement(handle, ref windowPlacement);
|
||||
}
|
||||
|
||||
bool micaApplied = new SystemBackdrop(this).TrySetBackdrop();
|
||||
logger.LogInformation(EventIds.BackdropState, "Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed");
|
||||
_ = new WindowManager(this, TitleBarView.DragableArea);
|
||||
}
|
||||
|
||||
private void MainWindowClosed(object sender, WindowEventArgs args)
|
||||
{
|
||||
SaveWindowRect(handle);
|
||||
|
||||
// save datebase
|
||||
// save userdata datebase
|
||||
int changes = appDbContext.SaveChanges();
|
||||
Verify.Operation(changes == 0, "存在未经处理的数据库记录更改");
|
||||
}
|
||||
|
||||
110
src/Snap.Hutao/Snap.Hutao/Model/Binding/User.cs
Normal file
110
src/Snap.Hutao/Snap.Hutao/Model/Binding/User.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.Model.Binding;
|
||||
|
||||
/// <summary>
|
||||
/// 用于视图绑定的用户
|
||||
/// </summary>
|
||||
public class User : Observable
|
||||
{
|
||||
private readonly Entity.User inner;
|
||||
|
||||
private UserGameRole? selectedUserGameRole;
|
||||
private bool isInitialized = false;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的绑定视图用户
|
||||
/// </summary>
|
||||
/// <param name="user">用户实体</param>
|
||||
private User(Entity.User user)
|
||||
{
|
||||
inner = user;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户信息
|
||||
/// </summary>
|
||||
public UserInfo? UserInfo { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户信息
|
||||
/// </summary>
|
||||
public List<UserGameRole> UserGameRoles { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 用户信息
|
||||
/// </summary>
|
||||
public UserGameRole? SelectedUserGameRole
|
||||
{
|
||||
get => selectedUserGameRole;
|
||||
private set => Set(ref selectedUserGameRole, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Entity.User.IsSelected"/>
|
||||
public bool IsSelected
|
||||
{
|
||||
get => inner.IsSelected;
|
||||
set => inner.IsSelected = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Entity.User.Cookie"/>
|
||||
public string? Cookie
|
||||
{
|
||||
get => inner.Cookie;
|
||||
set => inner.Cookie = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内部的用户实体
|
||||
/// </summary>
|
||||
public Entity.User Entity { get => inner; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否初始化完成
|
||||
/// </summary>
|
||||
public bool IsInitialized { get => isInitialized; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始化用户
|
||||
/// </summary>
|
||||
/// <param name="inner">用户实体</param>
|
||||
/// <param name="userClient">用户客户端</param>
|
||||
/// <param name="userGameRoleClient">角色客户端</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>用户是否初始化完成,若Cookie失效会返回 <see langword="false"/> </returns>
|
||||
internal static async Task<User?> CreateAsync(Entity.User inner, UserClient userClient, UserGameRoleClient userGameRoleClient, CancellationToken token = default)
|
||||
{
|
||||
User user = new(inner);
|
||||
bool successful = await user.InitializeAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
|
||||
return successful ? user : null;
|
||||
}
|
||||
|
||||
private async Task<bool> InitializeAsync(UserClient userClient, UserGameRoleClient userGameRoleClient, CancellationToken token = default)
|
||||
{
|
||||
if (isInitialized)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
UserInfo = await userClient
|
||||
.GetUserFullInfoAsync(this, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
UserGameRoles = await userGameRoleClient
|
||||
.GetUserGameRolesAsync(this, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
SelectedUserGameRole = UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen);
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
return UserInfo != null && UserGameRoles.Any();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace Snap.Hutao.Model.Entity;
|
||||
|
||||
@@ -18,15 +10,8 @@ namespace Snap.Hutao.Model.Entity;
|
||||
/// 用户
|
||||
/// </summary>
|
||||
[Table("users")]
|
||||
public class User : Observable
|
||||
public class User
|
||||
{
|
||||
/// <summary>
|
||||
/// 无用户
|
||||
/// </summary>
|
||||
public static readonly User None = new();
|
||||
private bool isInitialized = false;
|
||||
private UserGameRole? selectedUserGameRole;
|
||||
|
||||
/// <summary>
|
||||
/// 内部Id
|
||||
/// </summary>
|
||||
@@ -44,44 +29,6 @@ public class User : Observable
|
||||
/// </summary>
|
||||
public string? Cookie { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户信息
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public UserInfo? UserInfo { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户信息
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public List<UserGameRole>? UserGameRoles { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户信息
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public UserGameRole? SelectedUserGameRole
|
||||
{
|
||||
get => selectedUserGameRole;
|
||||
private set => Set(ref selectedUserGameRole, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复制Cookie命令
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public ICommand? CopyCookieCommand { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 判断用户是否为空用户
|
||||
/// </summary>
|
||||
/// <param name="user">待检测的用户</param>
|
||||
/// <returns>是否为空用户</returns>
|
||||
public static bool IsNone([NotNullWhen(false)] User? user)
|
||||
{
|
||||
return ReferenceEquals(NoneIfNullOrNoCookie(user), None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置用户的选中状态
|
||||
/// 同时更新用户选择的角色信息
|
||||
@@ -90,83 +37,6 @@ public class User : Observable
|
||||
/// <param name="isSelected">是否选中</param>
|
||||
public static void SetSelectionState(User user, bool isSelected)
|
||||
{
|
||||
Verify.Operation(!IsNone(user), "尝试设置一个空的用户");
|
||||
|
||||
user!.IsSelected = isSelected;
|
||||
if (isSelected)
|
||||
{
|
||||
user.SelectedUserGameRole ??= user.UserGameRoles!.FirstOrFirstOrDefault(role => role.IsChosen);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化此用户
|
||||
/// 初始化前必须设置 <see cref="RemoveCommand"/> 属性
|
||||
/// </summary>
|
||||
/// <param name="userClient">用户客户端</param>
|
||||
/// <param name="userGameRoleClient">角色客户端</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>用户是否初始化完成,若Cookie失效会返回 <see langword="false"/> </returns>
|
||||
internal async Task<bool> InitializeAsync(UserClient userClient, UserGameRoleClient userGameRoleClient, CancellationToken token = default)
|
||||
{
|
||||
if (IsNone(this))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isInitialized)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
CopyCookieCommand = new RelayCommand(CopyCookie);
|
||||
|
||||
UserInfo = await userClient
|
||||
.GetUserFullInfoAsync(this, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
UserGameRoles = await userGameRoleClient
|
||||
.GetUserGameRolesAsync(this, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
SelectedUserGameRole = UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen);
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
return UserInfo != null && UserGameRoles.Any();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试尽可能转换为 <see cref="None"/>
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <returns>转换后的用户</returns>
|
||||
private static User NoneIfNullOrNoCookie(User? user)
|
||||
{
|
||||
if (user is null || user.Cookie == null)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
else
|
||||
{
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyCookie()
|
||||
{
|
||||
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
try
|
||||
{
|
||||
DataPackage content = new();
|
||||
content.SetText(Must.NotNull(Cookie!));
|
||||
Clipboard.SetContent(content);
|
||||
|
||||
infoBarService.Success($"{UserInfo?.Nickname} 的 Cookie 复制成功");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
infoBarService.Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,11 @@ namespace Snap.Hutao.Model.Metadata.Achievement;
|
||||
/// </summary>
|
||||
public class AchievementGoal
|
||||
{
|
||||
/// <summary>
|
||||
/// Id
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序顺序
|
||||
/// </summary>
|
||||
|
||||
@@ -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 AchievementIconConverter : IValueConverter
|
||||
{
|
||||
private const string BaseUrl = "https://static.snapgenshin.com/AchievementIcon/{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();
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,12 @@ internal class AvatarNameCardPicConverter : IValueConverter
|
||||
return new Uri(string.Format(BaseUrl, avatarName));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
|
||||
private static string ReplaceSpecialCaseNaming(string avatarName)
|
||||
{
|
||||
return avatarName switch
|
||||
@@ -33,10 +39,4 @@ internal class AvatarNameCardPicConverter : IValueConverter
|
||||
_ => avatarName,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ public class Pair<TKey, TValue>
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 键
|
||||
/// </summary>
|
||||
public TKey Key { get; set; }
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
<Resource Language="zh-CN"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Achievement;
|
||||
|
||||
/// <summary>
|
||||
/// 成就服务
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IAchievementService))]
|
||||
internal class AchievementService : IAchievementService
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Snap.Hutao.Service.Achievement;
|
||||
|
||||
/// <summary>
|
||||
/// 成就服务抽象
|
||||
/// </summary>
|
||||
internal interface IAchievementService
|
||||
{
|
||||
|
||||
}
|
||||
@@ -26,47 +26,47 @@ internal interface IMetadataService
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>成就列表</returns>
|
||||
ValueTask<IEnumerable<Achievement>> GetAchievementsAsync(CancellationToken token = default);
|
||||
ValueTask<List<Model.Metadata.Achievement.Achievement>> GetAchievementsAsync(CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取成就分类列表
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>成就分类列表</returns>
|
||||
ValueTask<IEnumerable<AchievementGoal>> GetAchievementGoalsAsync(CancellationToken token = default);
|
||||
ValueTask<List<AchievementGoal>> GetAchievementGoalsAsync(CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取角色列表
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色列表</returns>
|
||||
ValueTask<IEnumerable<Avatar>> GetAvatarsAsync(CancellationToken token = default);
|
||||
ValueTask<List<Avatar>> GetAvatarsAsync(CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取圣遗物列表
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>圣遗物列表</returns>
|
||||
ValueTask<IEnumerable<Reliquary>> GetReliquariesAsync(CancellationToken token = default);
|
||||
ValueTask<List<Reliquary>> GetReliquariesAsync(CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取圣遗物强化属性列表
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>圣遗物强化属性列表</returns>
|
||||
ValueTask<IEnumerable<ReliquaryAffix>> GetReliquaryAffixesAsync(CancellationToken token = default);
|
||||
ValueTask<List<ReliquaryAffix>> GetReliquaryAffixesAsync(CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取圣遗物主属性强化属性列表
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>圣遗物强化属性列表</returns>
|
||||
ValueTask<IEnumerable<ReliquaryAffixBase>> GetReliquaryMainAffixesAsync(CancellationToken token = default);
|
||||
ValueTask<List<ReliquaryAffixBase>> GetReliquaryMainAffixesAsync(CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取武器列表
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>武器列表</returns>
|
||||
ValueTask<IEnumerable<Weapon>> GetWeaponsAsync(CancellationToken token = default);
|
||||
}
|
||||
ValueTask<List<Weapon>> GetWeaponsAsync(CancellationToken token = default);
|
||||
}
|
||||
@@ -91,45 +91,45 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<IEnumerable<AchievementGoal>> GetAchievementGoalsAsync(CancellationToken token = default)
|
||||
public ValueTask<List<AchievementGoal>> GetAchievementGoalsAsync(CancellationToken token = default)
|
||||
{
|
||||
return GetMetadataAsync<IEnumerable<AchievementGoal>>("AchievementGoal", token);
|
||||
return GetMetadataAsync<List<AchievementGoal>>("AchievementGoal", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<IEnumerable<Achievement>> GetAchievementsAsync(CancellationToken token = default)
|
||||
public ValueTask<List<Model.Metadata.Achievement.Achievement>> GetAchievementsAsync(CancellationToken token = default)
|
||||
{
|
||||
return GetMetadataAsync<IEnumerable<Achievement>>("Achievement", token);
|
||||
return GetMetadataAsync<List<Model.Metadata.Achievement.Achievement>>("Achievement", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<IEnumerable<Avatar>> GetAvatarsAsync(CancellationToken token = default)
|
||||
public ValueTask<List<Avatar>> GetAvatarsAsync(CancellationToken token = default)
|
||||
{
|
||||
return GetMetadataAsync<IEnumerable<Avatar>>("Avatar", token);
|
||||
return GetMetadataAsync<List<Avatar>>("Avatar", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<IEnumerable<Reliquary>> GetReliquariesAsync(CancellationToken token = default)
|
||||
public ValueTask<List<Reliquary>> GetReliquariesAsync(CancellationToken token = default)
|
||||
{
|
||||
return GetMetadataAsync<IEnumerable<Reliquary>>("Reliquary", token);
|
||||
return GetMetadataAsync<List<Reliquary>>("Reliquary", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<IEnumerable<ReliquaryAffix>> GetReliquaryAffixesAsync(CancellationToken token = default)
|
||||
public ValueTask<List<ReliquaryAffix>> GetReliquaryAffixesAsync(CancellationToken token = default)
|
||||
{
|
||||
return GetMetadataAsync<IEnumerable<ReliquaryAffix>>("ReliquaryAffix", token);
|
||||
return GetMetadataAsync<List<ReliquaryAffix>>("ReliquaryAffix", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<IEnumerable<ReliquaryAffixBase>> GetReliquaryMainAffixesAsync(CancellationToken token = default)
|
||||
public ValueTask<List<ReliquaryAffixBase>> GetReliquaryMainAffixesAsync(CancellationToken token = default)
|
||||
{
|
||||
return GetMetadataAsync<IEnumerable<ReliquaryAffixBase>>("ReliquaryMainAffix", token);
|
||||
return GetMetadataAsync<List<ReliquaryAffixBase>>("ReliquaryMainAffix", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<IEnumerable<Weapon>> GetWeaponsAsync(CancellationToken token = default)
|
||||
public ValueTask<List<Weapon>> GetWeaponsAsync(CancellationToken token = default)
|
||||
{
|
||||
return GetMetadataAsync<IEnumerable<Weapon>>("Weapon", token);
|
||||
return GetMetadataAsync<List<Weapon>>("Weapon", token);
|
||||
}
|
||||
|
||||
private async Task<bool> TryUpdateMetadataAsync(CancellationToken token = default)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Binding;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
@@ -13,25 +13,24 @@ namespace Snap.Hutao.Service.Abstraction;
|
||||
public interface IUserService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前用户信息
|
||||
/// 获取或设置当前用户
|
||||
/// </summary>
|
||||
User? CurrentUser { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取用户信息枚举
|
||||
/// 每个用户信息都应准备完成
|
||||
/// 异步获取同步的用户信息集合
|
||||
/// 对集合的操作应通过服务抽象完成
|
||||
/// 此操作不能取消
|
||||
/// </summary>
|
||||
/// <param name="removeCommand">移除用户命令</param>
|
||||
/// <returns>准备完成的用户信息枚举</returns>
|
||||
Task<ObservableCollection<User>> GetInitializedUsersAsync();
|
||||
Task<ObservableCollection<User>> GetUserCollectionAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 异步添加用户
|
||||
/// 通常用户是未初始化的
|
||||
/// </summary>
|
||||
/// <param name="user">待添加的用户</param>
|
||||
/// <param name="uid">用户的米游社UID</param>
|
||||
/// <param name="uid">用户的米游社UID,用于检查是否包含重复的用户</param>
|
||||
/// <returns>用户初始化是否成功</returns>
|
||||
Task<UserAddResult> TryAddUserAsync(User user, string uid);
|
||||
|
||||
@@ -48,4 +47,11 @@ public interface IUserService
|
||||
/// <param name="cookie">cookie的字符串形式</param>
|
||||
/// <returns>包含cookie信息的字典</returns>
|
||||
IDictionary<string, string> ParseCookie(string cookie);
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个新的绑定用户
|
||||
/// </summary>
|
||||
/// <param name="cookie">cookie的字符串形式</param>
|
||||
/// <returns>新的绑定用户</returns>
|
||||
Task<User?> CreateUserAsync(string cookie);
|
||||
}
|
||||
|
||||
@@ -22,9 +22,4 @@ public enum UserAddResult
|
||||
/// 已经存在该用户
|
||||
/// </summary>
|
||||
AlreadyExists,
|
||||
|
||||
/// <summary>
|
||||
/// 初始化用户失败
|
||||
/// </summary>
|
||||
InitializeFailed,
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Context.Database;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
@@ -15,16 +13,17 @@ namespace Snap.Hutao.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 用户服务
|
||||
/// 主要负责将用户数据与数据库同步
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IUserService))]
|
||||
[Injection(InjectAs.Singleton, typeof(IUserService))]
|
||||
internal class UserService : IUserService
|
||||
{
|
||||
private readonly AppDbContext appDbContext;
|
||||
private readonly UserClient userClient;
|
||||
private readonly UserGameRoleClient userGameRoleClient;
|
||||
|
||||
private User? currentUser;
|
||||
private ObservableCollection<User>? cachedUsers = null;
|
||||
private Model.Binding.User? currentUser;
|
||||
private ObservableCollection<Model.Binding.User>? userCollection = null;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的用户服务
|
||||
@@ -40,18 +39,18 @@ internal class UserService : IUserService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public User? CurrentUser
|
||||
public Model.Binding.User? CurrentUser
|
||||
{
|
||||
get => currentUser;
|
||||
set
|
||||
{
|
||||
// only update when not processing a deletion
|
||||
if (!User.IsNone(value))
|
||||
if (value != null)
|
||||
{
|
||||
if (!User.IsNone(currentUser))
|
||||
if (currentUser != null)
|
||||
{
|
||||
User.SetSelectionState(currentUser, false);
|
||||
appDbContext.Users.Update(currentUser);
|
||||
currentUser.IsSelected = false;
|
||||
appDbContext.Users.Update(currentUser.Entity);
|
||||
appDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
@@ -59,91 +58,98 @@ internal class UserService : IUserService
|
||||
// 当删除到无用户时也能正常反应状态
|
||||
currentUser = value;
|
||||
|
||||
if (!User.IsNone(currentUser))
|
||||
if (currentUser != null)
|
||||
{
|
||||
User.SetSelectionState(currentUser, true);
|
||||
appDbContext.Users.Update(currentUser);
|
||||
currentUser.IsSelected = true;
|
||||
appDbContext.Users.Update(currentUser.Entity);
|
||||
appDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<UserAddResult> TryAddUserAsync(User newUser, string uid)
|
||||
public async Task<UserAddResult> TryAddUserAsync(Model.Binding.User newUser, string uid)
|
||||
{
|
||||
Must.NotNull(cachedUsers!);
|
||||
Must.NotNull(userCollection!);
|
||||
|
||||
// 查找是否有相同的uid
|
||||
User? userWithSameUid = cachedUsers
|
||||
.SingleOrDefault(u => u.UserInfo!.Uid == uid);
|
||||
|
||||
if (userWithSameUid != null)
|
||||
if (userCollection.SingleOrDefault(u => u.UserInfo!.Uid == uid) is Model.Binding.User userWithSameUid)
|
||||
{
|
||||
// Prevent users from adding same cookie.
|
||||
// Prevent users from adding a completely same cookie.
|
||||
if (userWithSameUid.Cookie == newUser.Cookie)
|
||||
{
|
||||
return UserAddResult.AlreadyExists;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try update user here.
|
||||
// Update user cookie here.
|
||||
userWithSameUid.Cookie = newUser.Cookie;
|
||||
appDbContext.Users.Update(userWithSameUid);
|
||||
|
||||
await appDbContext
|
||||
.SaveChangesAsync()
|
||||
.ConfigureAwait(false);
|
||||
appDbContext.Users.Update(userWithSameUid.Entity);
|
||||
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
return UserAddResult.Updated;
|
||||
}
|
||||
}
|
||||
|
||||
// must continue on the caller thread.
|
||||
if (await newUser.InitializeAsync(userClient, userGameRoleClient))
|
||||
else
|
||||
{
|
||||
appDbContext.Users.Add(newUser);
|
||||
Verify.Operation(newUser.IsInitialized, "该用户尚未初始化");
|
||||
|
||||
await appDbContext
|
||||
.SaveChangesAsync()
|
||||
.ConfigureAwait(false);
|
||||
// Sync database
|
||||
appDbContext.Users.Add(newUser.Entity);
|
||||
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
// Sync cache
|
||||
userCollection.Add(newUser);
|
||||
|
||||
return UserAddResult.Added;
|
||||
}
|
||||
|
||||
return UserAddResult.InitializeFailed;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task RemoveUserAsync(User user)
|
||||
public Task RemoveUserAsync(Model.Binding.User user)
|
||||
{
|
||||
appDbContext.Users.Remove(user);
|
||||
userCollection!.Remove(user);
|
||||
appDbContext.Users.Remove(user.Entity);
|
||||
return appDbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ObservableCollection<User>> GetInitializedUsersAsync()
|
||||
public async Task<ObservableCollection<Model.Binding.User>> GetUserCollectionAsync()
|
||||
{
|
||||
if (cachedUsers == null)
|
||||
if (userCollection == null)
|
||||
{
|
||||
await appDbContext.Users
|
||||
.LoadAsync()
|
||||
.ConfigureAwait(false);
|
||||
List<Model.Binding.User> users = new();
|
||||
|
||||
cachedUsers = appDbContext.Users.Local.ToObservableCollection();
|
||||
|
||||
foreach (User user in cachedUsers)
|
||||
foreach (Model.Entity.User user in appDbContext.Users)
|
||||
{
|
||||
await user
|
||||
.InitializeAsync(userClient, userGameRoleClient)
|
||||
Model.Binding.User? initialized = await Model.Binding.User
|
||||
.CreateAsync(user, userClient, userGameRoleClient)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (initialized != null)
|
||||
{
|
||||
users.Add(initialized);
|
||||
}
|
||||
else
|
||||
{
|
||||
// User is unable to be initialized, remove it.
|
||||
appDbContext.Users.Remove(user);
|
||||
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
CurrentUser = await appDbContext.Users
|
||||
.SingleOrDefaultAsync(user => user.IsSelected)
|
||||
.ConfigureAwait(false);
|
||||
CurrentUser = users.SingleOrDefault(user => user.IsSelected);
|
||||
|
||||
userCollection = new(users);
|
||||
}
|
||||
|
||||
return cachedUsers;
|
||||
return userCollection;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<Model.Binding.User?> CreateUserAsync(string cookie)
|
||||
{
|
||||
return Model.Binding.User.CreateAsync(new() { Cookie = cookie }, userClient, userGameRoleClient);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
<ItemsControl
|
||||
x:Name="DetailsHost"
|
||||
VerticalAlignment="Top">
|
||||
<ItemsControl.ItemContainerTransitions>
|
||||
<ContentThemeTransition/>
|
||||
</ItemsControl.ItemContainerTransitions>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<sc:Setting Margin="0,2,0,0" Header="{Binding Description}" Padding="12,0">
|
||||
|
||||
@@ -44,9 +44,7 @@
|
||||
|
||||
<Frame x:Name="ContentFrame">
|
||||
<Frame.ContentTransitions>
|
||||
<TransitionCollection>
|
||||
<NavigationThemeTransition/>
|
||||
</TransitionCollection>
|
||||
<NavigationThemeTransition/>
|
||||
</Frame.ContentTransitions>
|
||||
</Frame>
|
||||
</NavigationView>
|
||||
|
||||
@@ -5,28 +5,116 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:sc="using:SettingsUI.Controls"
|
||||
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
||||
xmlns:shcc="using:Snap.Hutao.Control.Cancellable"
|
||||
xmlns:shci="using:Snap.Hutao.Control.Image"
|
||||
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
|
||||
xmlns:shv="using:Snap.Hutao.ViewModel"
|
||||
mc:Ignorable="d"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
d:DataContext="{d:DesignInstance shv:AchievementViewModel}">
|
||||
|
||||
<shcc:CancellablePage.Resources>
|
||||
<shmmc:AchievementIconConverter x:Key="AchievementIconConverter"/>
|
||||
</shcc:CancellablePage.Resources>
|
||||
|
||||
<mxi:Interaction.Behaviors>
|
||||
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
|
||||
</mxi:Interaction.Behaviors>
|
||||
<Grid>
|
||||
<ScrollViewer>
|
||||
<ItemsControl
|
||||
Margin="12,0,16,12"
|
||||
ItemsSource="{Binding AchievementsView}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<sc:Setting
|
||||
Margin="0,12,0,0"
|
||||
Header="{Binding Title}"
|
||||
Description="{Binding Description}"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
<SplitView
|
||||
IsPaneOpen="True"
|
||||
DisplayMode="Inline"
|
||||
OpenPaneLength="252">
|
||||
<SplitView.PaneBackground>
|
||||
<SolidColorBrush Color="{ThemeResource CardBackgroundFillColorSecondary}"/>
|
||||
</SplitView.PaneBackground>
|
||||
<SplitView.Pane>
|
||||
<ListView
|
||||
SelectionMode="Single"
|
||||
SelectedItem="{Binding SelectedAchievementGoal,Mode=TwoWay}"
|
||||
ItemsSource="{Binding AchievementGoals}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="24"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shci:CachedImage
|
||||
Width="24"
|
||||
Height="24"
|
||||
Grid.Column="0"
|
||||
Source="{Binding Icon,Converter={StaticResource AchievementIconConverter}}"/>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Grid.Column="1"
|
||||
Margin="12,0,0,2"
|
||||
Text="{Binding Name}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</SplitView.Pane>
|
||||
|
||||
<SplitView.Content>
|
||||
<ScrollViewer Padding="0,0,16,0">
|
||||
<ItemsControl
|
||||
Margin="16,0,0,16"
|
||||
ItemsSource="{Binding Achievements}">
|
||||
<!--ContentThemeTransition here can make items blinking,
|
||||
cause we are using ItemsStackPanel-->
|
||||
<!--<ItemsControl.Transitions>
|
||||
<ContentThemeTransition/>
|
||||
</ItemsControl.Transitions>-->
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<ItemsStackPanel/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}"
|
||||
Background="{ThemeResource CardBackgroundBrush}"
|
||||
BorderThickness="{ThemeResource CardBorderThickness}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
Padding="8"
|
||||
Margin="0,16,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
MinHeight="48">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<!-- Icon -->
|
||||
<ColumnDefinition Width="*"/>
|
||||
<!-- Header and subtitle -->
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<!-- Action control -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<CheckBox
|
||||
Margin="6,0,0,0"
|
||||
Style="{StaticResource DefaultCheckBoxStyle}"
|
||||
Padding="16,0,0,0"
|
||||
Grid.Column="1">
|
||||
<CheckBox.Content>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding Title}"/>
|
||||
<TextBlock
|
||||
Margin="0,2,0,0"
|
||||
Style="{StaticResource SecondaryTextStyle}"
|
||||
Text="{Binding Description}"/>
|
||||
</StackPanel>
|
||||
</CheckBox.Content>
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</SplitView.Content>
|
||||
</SplitView>
|
||||
|
||||
</Grid>
|
||||
</shcc:CancellablePage>
|
||||
</shcc:CancellablePage>
|
||||
@@ -16,17 +16,21 @@ namespace Snap.Hutao.View.Page;
|
||||
/// </summary>
|
||||
public sealed partial class AnnouncementContentPage : Microsoft.UI.Xaml.Controls.Page
|
||||
{
|
||||
// apply in dark mode
|
||||
private const string LightColor1 = "color:rgba(255,255,255,1)";
|
||||
private const string LightColor2 = "color:rgba(238,238,238,1)";
|
||||
private const string LightColor3 = "color:rgba(204,204,204,1)";
|
||||
private const string LightColor4 = "color:rgba(198,196,191,1)";
|
||||
private const string LightColor5 = "color:rgba(170,170,170,1)";
|
||||
private const string LightAccentColor = "background-color: rgb(0,40,70)";
|
||||
|
||||
// find in content
|
||||
private const string DarkColor1 = "color:rgba(0,0,0,1)";
|
||||
private const string DarkColor2 = "color:rgba(17,17,17,1)";
|
||||
private const string DarkColor3 = "color:rgba(51,51,51,1)";
|
||||
private const string DarkColor4 = "color:rgba(57,59,64,1)";
|
||||
private const string DarkColor5 = "color:rgba(85,85,85,1)";
|
||||
private const string DarkAccentColor1 = "background-color: rgb(255, 215, 185);";
|
||||
|
||||
// support click open browser.
|
||||
private const string MihoyoSDKDefinition =
|
||||
@@ -76,7 +80,8 @@ openInWebview: function(url){ location.href = url }}";
|
||||
.Replace(DarkColor5, LightColor5)
|
||||
.Replace(DarkColor4, LightColor4)
|
||||
.Replace(DarkColor3, LightColor3)
|
||||
.Replace(DarkColor2, LightColor2);
|
||||
.Replace(DarkColor2, LightColor2)
|
||||
.Replace(DarkAccentColor1, LightAccentColor);
|
||||
}
|
||||
|
||||
// wrap a default color body around
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
d:DataContext="{d:DesignInstance shv:SettingViewModel}">
|
||||
<Page.Resources>
|
||||
<Style TargetType="Button" BasedOn="{StaticResource DefaultButtonStyle}">
|
||||
<Style TargetType="Button" BasedOn="{StaticResource SettingButtonStyle}">
|
||||
<Setter Property="MinWidth" Value="160"/>
|
||||
</Style>
|
||||
</Page.Resources>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.WinUI.UI;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
using Snap.Hutao.Control.Cancellable;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Model.Metadata.Achievement;
|
||||
@@ -20,7 +19,9 @@ namespace Snap.Hutao.ViewModel;
|
||||
internal class AchievementViewModel : ObservableObject, ISupportCancellation
|
||||
{
|
||||
private readonly IMetadataService metadataService;
|
||||
private AdvancedCollectionView? achievementsView;
|
||||
private AdvancedCollectionView? achievements;
|
||||
private IList<AchievementGoal>? achievementGoals;
|
||||
private AchievementGoal? selectedAchievementGoal;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的成就视图模型
|
||||
@@ -39,10 +40,32 @@ internal class AchievementViewModel : ObservableObject, ISupportCancellation
|
||||
/// <summary>
|
||||
/// 成就视图
|
||||
/// </summary>
|
||||
public AdvancedCollectionView? AchievementsView
|
||||
public AdvancedCollectionView? Achievements
|
||||
{
|
||||
get => achievementsView;
|
||||
set => SetProperty(ref achievementsView, value);
|
||||
get => achievements;
|
||||
set => SetProperty(ref achievements, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 成就分类
|
||||
/// </summary>
|
||||
public IList<AchievementGoal>? AchievementGoals
|
||||
{
|
||||
get => achievementGoals;
|
||||
set => SetProperty(ref achievementGoals, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 选中的成就分类
|
||||
/// </summary>
|
||||
public AchievementGoal? SelectedAchievementGoal
|
||||
{
|
||||
get => selectedAchievementGoal;
|
||||
set
|
||||
{
|
||||
SetProperty(ref selectedAchievementGoal, value);
|
||||
OnGoalChanged(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -50,17 +73,22 @@ internal class AchievementViewModel : ObservableObject, ISupportCancellation
|
||||
/// </summary>
|
||||
public ICommand OpenUICommand { get; }
|
||||
|
||||
private async Task OpenUIAsync(CancellationToken token)
|
||||
private async Task OpenUIAsync()
|
||||
{
|
||||
using (CancellationTokenExtensions.CombinedCancellationToken combined = token.CombineWith(CancellationToken))
|
||||
if (await metadataService.InitializeAsync(CancellationToken))
|
||||
{
|
||||
if (await metadataService.InitializeAsync(combined.Token))
|
||||
{
|
||||
IEnumerable<Achievement> achievements = await metadataService.GetAchievementsAsync(combined.Token);
|
||||
|
||||
// TODO
|
||||
AchievementsView = new(achievements.ToList());
|
||||
}
|
||||
Achievements = new(await metadataService.GetAchievementsAsync(CancellationToken), true);
|
||||
AchievementGoals = await metadataService.GetAchievementGoalsAsync(CancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGoalChanged(AchievementGoal? goal)
|
||||
{
|
||||
if (Achievements != null)
|
||||
{
|
||||
Achievements.Filter = goal != null
|
||||
? ((object o) => o is Achievement achi && achi.Goal == goal.Id)
|
||||
: ((object o) => true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
|
||||
|
||||
private Task OpenCacheFolderAsync(CancellationToken token)
|
||||
{
|
||||
return Launcher.LaunchFolderAsync(App.Current.CacheFolder).AsTask(token);
|
||||
return Launcher.LaunchFolderAsync(App.CacheFolder).AsTask(token);
|
||||
}
|
||||
|
||||
private Task OpenDataFolderAsync(CancellationToken token)
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Binding;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using System.Collections.Generic;
|
||||
@@ -27,7 +28,7 @@ internal class UserViewModel : ObservableObject
|
||||
private readonly IInfoBarService infoBarService;
|
||||
|
||||
private User? selectedUser;
|
||||
private ObservableCollection<User>? userInfos;
|
||||
private ObservableCollection<User>? users;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的用户视图模型
|
||||
@@ -64,7 +65,7 @@ internal class UserViewModel : ObservableObject
|
||||
/// <summary>
|
||||
/// 用户信息集合
|
||||
/// </summary>
|
||||
public ObservableCollection<User>? Users { get => userInfos; set => SetProperty(ref userInfos, value); }
|
||||
public ObservableCollection<User>? Users { get => users; set => SetProperty(ref users, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 打开界面命令
|
||||
@@ -90,20 +91,20 @@ internal class UserViewModel : ObservableObject
|
||||
{
|
||||
int validFlag = 4;
|
||||
|
||||
filteredCookie = new SortedDictionary<string, string>();
|
||||
SortedDictionary<string, string> filter = new();
|
||||
|
||||
// O(1) to validate cookie
|
||||
foreach ((string key, string value) in map)
|
||||
{
|
||||
if (key == AccountIdKey || key == "cookie_token" || key == "ltoken" || key == "ltuid")
|
||||
{
|
||||
validFlag--;
|
||||
filteredCookie.Add(key, value);
|
||||
filter.Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (validFlag == 0)
|
||||
{
|
||||
filteredCookie = filter;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
@@ -115,41 +116,43 @@ internal class UserViewModel : ObservableObject
|
||||
|
||||
private async Task OpenUIAsync()
|
||||
{
|
||||
Users = await userService.GetInitializedUsersAsync();
|
||||
Users = await userService.GetUserCollectionAsync();
|
||||
SelectedUser = userService.CurrentUser;
|
||||
}
|
||||
|
||||
private async Task AddUserAsync()
|
||||
{
|
||||
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
// Get cookie from user input
|
||||
Window mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
Result<bool, string> result = await new UserDialog(mainWindow).GetInputCookieAsync();
|
||||
|
||||
// user confirms the input
|
||||
// User confirms the input
|
||||
if (result.IsOk)
|
||||
{
|
||||
IDictionary<string, string> cookieMap = userService.ParseCookie(result.Value);
|
||||
|
||||
if (TryValidateCookie(cookieMap, out IDictionary<string, string>? filteredCookie))
|
||||
if (TryValidateCookie(userService.ParseCookie(result.Value), out IDictionary<string, string>? filteredCookie))
|
||||
{
|
||||
string simplifiedCookie = string.Join(';', filteredCookie.Select(kvp => $"{kvp.Key}={kvp.Value}"));
|
||||
User user = new() { Cookie = simplifiedCookie };
|
||||
|
||||
switch (await userService.TryAddUserAsync(user, filteredCookie[AccountIdKey]))
|
||||
if (await userService.CreateUserAsync(simplifiedCookie) is User user)
|
||||
{
|
||||
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 无法获取用户信息,请重新输入");
|
||||
break;
|
||||
default:
|
||||
throw Must.NeverHappen();
|
||||
switch (await userService.TryAddUserAsync(user, filteredCookie[AccountIdKey]))
|
||||
{
|
||||
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;
|
||||
default:
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning("此 Cookie 无法获取用户信息,请重新输入");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -161,19 +164,14 @@ internal class UserViewModel : ObservableObject
|
||||
|
||||
private async Task RemoveUserAsync(User? user)
|
||||
{
|
||||
if (!User.IsNone(user))
|
||||
{
|
||||
await userService.RemoveUserAsync(user);
|
||||
infoBarService.Success($"用户 [{user.UserInfo!.Nickname}] 成功移除");
|
||||
}
|
||||
Verify.Operation(user != null, "待删除的用户不应为 null");
|
||||
await userService.RemoveUserAsync(user);
|
||||
infoBarService.Success($"用户 [{user.UserInfo?.Nickname}] 成功移除");
|
||||
}
|
||||
|
||||
private void CopyCookie(User? user)
|
||||
{
|
||||
if (User.IsNone(user))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Verify.Operation(user != null, "待复制 Cookie 的用户不应为 null");
|
||||
|
||||
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
try
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace Snap.Hutao.ViewModel;
|
||||
internal class WikiAvatarViewModel : ObservableObject
|
||||
{
|
||||
private readonly IMetadataService metadataService;
|
||||
|
||||
// filters
|
||||
private readonly List<Selectable<string>> filterElementInfos;
|
||||
private readonly List<Selectable<Pair<string, string>>> filterAssociationInfos;
|
||||
private readonly List<Selectable<Pair<string, WeaponType>>> filterWeaponTypeInfos;
|
||||
@@ -129,12 +131,12 @@ internal class WikiAvatarViewModel : ObservableObject
|
||||
{
|
||||
if (await metadataService.InitializeAsync())
|
||||
{
|
||||
IEnumerable<Avatar>? avatars = await metadataService.GetAvatarsAsync();
|
||||
IList<Avatar> avatars = await metadataService.GetAvatarsAsync();
|
||||
IOrderedEnumerable<Avatar> sorted = avatars
|
||||
.OrderBy(avatar => avatar.BeginTime)
|
||||
.ThenBy(avatar => avatar.Sort);
|
||||
|
||||
Avatars = new AdvancedCollectionView(new List<Avatar>(sorted), true);
|
||||
Avatars = new AdvancedCollectionView(sorted.ToList(), true);
|
||||
Avatars.MoveCurrentToFirst();
|
||||
}
|
||||
}
|
||||
@@ -175,7 +177,10 @@ internal class WikiAvatarViewModel : ObservableObject
|
||||
&& targeQualities.Contains(avatar.Quality)
|
||||
&& targetBodies.Contains(avatar.Body);
|
||||
|
||||
Avatars.MoveCurrentToFirst();
|
||||
if (!Avatars.Contains(Selected))
|
||||
{
|
||||
Avatars.MoveCurrentToFirst();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ internal class UserClient
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>详细信息</returns>
|
||||
public async Task<UserInfo?> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default)
|
||||
public async Task<UserInfo?> GetUserFullInfoAsync(Model.Binding.User user, CancellationToken token = default)
|
||||
{
|
||||
Response<UserFullInfoWrapper>? resp = await httpClient
|
||||
.UsingDynamicSecret()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Binding;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab;
|
||||
@@ -19,11 +19,7 @@ internal static class HttpClientCookieExtensions
|
||||
/// <returns>客户端</returns>
|
||||
internal static HttpClient SetUser(this HttpClient httpClient, User user)
|
||||
{
|
||||
if (!User.IsNone(user))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie);
|
||||
}
|
||||
|
||||
httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie);
|
||||
return httpClient;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model.Binding;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
@@ -33,7 +34,7 @@ internal class UserGameRoleClient
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>用户角色信息</returns>
|
||||
public async Task<List<UserGameRole>> GetUserGameRolesAsync(Model.Entity.User user, CancellationToken token = default)
|
||||
public async Task<List<UserGameRole>> GetUserGameRolesAsync(User user, CancellationToken token = default)
|
||||
{
|
||||
Response<ListWrapper<UserGameRole>>? resp = await httpClient
|
||||
.SetUser(user)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Binding;
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
|
||||
using Snap.Hutao.Web.Response;
|
||||
|
||||
@@ -294,7 +294,7 @@ internal class HutaoClient : ISupportAsyncInitialization
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>玩家记录</returns>
|
||||
public async Task<PlayerRecord> GetPlayerRecordAsync(User user, CancellationToken token = default)
|
||||
public async Task<PlayerRecord> GetPlayerRecordAsync(Snap.Hutao.Model.Binding.User user, CancellationToken token = default)
|
||||
{
|
||||
PlayerInfo? playerInfo = await gameRecordClient
|
||||
.GetPlayerInfoAsync(user, token)
|
||||
|
||||
Reference in New Issue
Block a user