refactor user infrastructure

This commit is contained in:
DismissedLight
2022-08-01 13:46:50 +08:00
parent 41d99c227c
commit acb59488fb
48 changed files with 660 additions and 492 deletions

View File

@@ -35,13 +35,14 @@ public class CheckBoxWithDescriptionControl : CheckBox
private void CheckBoxSubTextControl_Loaded(object sender, RoutedEventArgs e) 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. // Add text box only if the description is not empty. Required for additional plugin options.
if (!string.IsNullOrWhiteSpace(Description)) if (!string.IsNullOrWhiteSpace(Description))
{ {
panel.Children.Add(new TextBlock() { Margin = new Thickness(0, 10, 0, 0), Text = Header }); 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 else
{ {

View File

@@ -7,9 +7,9 @@
<Style x:Key="SettingButtonStyle" TargetType="Button" BasedOn="{StaticResource DefaultButtonStyle}" > <Style x:Key="SettingButtonStyle" TargetType="Button" BasedOn="{StaticResource DefaultButtonStyle}" >
<Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}" /> <Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" /> <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="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Stretch"/> <Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style> </Style>
<Style x:Key="HyperlinkButtonStyle" TargetType="HyperlinkButton" > <Style x:Key="HyperlinkButtonStyle" TargetType="HyperlinkButton" >

View File

@@ -28,8 +28,4 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="CoreEnvironment\" />
</ItemGroup>
</Project> </Project>

View File

@@ -28,6 +28,7 @@ public partial class App : Application
/// </summary> /// </summary>
public App() public App()
{ {
AppInstance.GetCurrent().Activated += OnActivated;
// load app resource // load app resource
InitializeComponent(); InitializeComponent();
InitializeDependencyInjection(); InitializeDependencyInjection();
@@ -52,16 +53,24 @@ public partial class App : Application
} }
/// <summary> /// <summary>
/// <inheritdoc cref="Windows.Storage.ApplicationData.Current"/> /// <inheritdoc cref="ApplicationData.Current.TemporaryFolder"/>
/// </summary> /// </summary>
[SuppressMessage("", "CA1822")] public static StorageFolder CacheFolder
public StorageFolder CacheFolder
{ {
get => ApplicationData.Current.TemporaryFolder; get => ApplicationData.Current.TemporaryFolder;
} }
/// <summary>
/// <inheritdoc cref="ApplicationData.Current.LocalSettings"/>
/// </summary>
public static ApplicationDataContainer Settings
{
get => ApplicationData.Current.LocalSettings;
}
/// <summary> /// <summary>
/// Invoked when the application is launched. /// Invoked when the application is launched.
/// Any async operation in this method should be wrapped with try catch
/// </summary> /// </summary>
/// <param name="args">Details about the launch request and process.</param> /// <param name="args">Details about the launch request and process.</param>
[SuppressMessage("", "VSTHRD100")] [SuppressMessage("", "VSTHRD100")]
@@ -69,7 +78,6 @@ public partial class App : Application
{ {
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
AppInstance firstInstance = AppInstance.FindOrRegisterForKey("main"); AppInstance firstInstance = AppInstance.FindOrRegisterForKey("main");
firstInstance.Activated += OnActivated;
if (!firstInstance.IsCurrent) if (!firstInstance.IsCurrent)
{ {
@@ -83,7 +91,6 @@ public partial class App : Application
Window.Activate(); Window.Activate();
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", CacheFolder.Path); logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", CacheFolder.Path);
logger.LogInformation(EventIds.CommonLog, "Data folder : {folder}", CacheFolder.Path);
Ioc.Default Ioc.Default
.GetRequiredService<IMetadataService>() .GetRequiredService<IMetadataService>()
@@ -117,19 +124,22 @@ public partial class App : Application
Ioc.Default.ConfigureServices(services); 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) 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) private void XamlBindingFailed(object sender, BindingFailedEventArgs e)
{ {
logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message); logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message);

View File

@@ -5,7 +5,9 @@ using CommunityToolkit.WinUI.UI.Controls;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Core.Caching; using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.Exception;
using Snap.Hutao.Extension; using Snap.Hutao.Extension;
using System.Runtime.InteropServices;
using Windows.Storage; using Windows.Storage;
namespace Snap.Hutao.Control.Image; namespace Snap.Hutao.Control.Image;
@@ -25,30 +27,34 @@ public class CachedImage : ImageEx
} }
/// <inheritdoc/> /// <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>(); IImageCache imageCache = Ioc.Default.GetRequiredService<IImageCache>();
try try
{ {
Verify.Operation(imageUri.Host != string.Empty, "可能是空绑定产生的 [ms-appx:///]"); Verify.Operation(imageUri.Host != string.Empty, "无效的Uri");
StorageFile file = await imageCache.GetFileFromCacheAsync(imageUri); StorageFile file = await imageCache.GetFileFromCacheAsync(imageUri);
// check token state to determine whether the operation should be canceled. // 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)); 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) catch (TaskCanceledException)
{ {
// task was explicitly canceled // task was explicitly canceled
throw; return null;
} }
catch catch
{ {
// maybe the image is corrupted, remove it.
await imageCache.RemoveAsync(imageUri.Enumerate()).ConfigureAwait(false);
throw; throw;
} }
} }

View File

@@ -8,6 +8,7 @@ using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Core; using Snap.Hutao.Core;
using Snap.Hutao.Core.Caching; using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.Exception;
using Snap.Hutao.Core.Threading; using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension; using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Abstraction;
@@ -72,9 +73,8 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
return LoadedImageSurface.StartLoadFromStream(imageStream); 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; return null;
} }
} }

View File

@@ -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);
}
}
}

View File

@@ -216,7 +216,7 @@ public abstract class CacheBase<T>
using (await cacheFolderSemaphore.EnterAsync().ConfigureAwait(false)) using (await cacheFolderSemaphore.EnterAsync().ConfigureAwait(false))
{ {
baseFolder ??= App.Current.CacheFolder; baseFolder ??= App.CacheFolder;
if (string.IsNullOrWhiteSpace(cacheFolderName)) if (string.IsNullOrWhiteSpace(cacheFolderName))
{ {

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Core;
/// <summary> /// <summary>
/// 核心环境参数 /// 核心环境参数
/// </summary> /// </summary>
internal static partial class CoreEnvironment internal static class CoreEnvironment
{ {
/// <summary> /// <summary>
/// 当前版本 /// 当前版本
@@ -43,8 +43,3 @@ internal static partial class CoreEnvironment
return Md5Convert.ToHexString($"{userName}{machineGuid}"); return Md5Convert.ToHexString($"{userName}{machineGuid}");
} }
} }
internal static partial class CoreEnvironment
{
}

View 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,
}

View File

@@ -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);
}
}

View File

@@ -41,7 +41,7 @@ internal sealed partial class DatebaseLogger : ILogger
} }
/// <inheritdoc /> /// <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)) if (!IsEnabled(logLevel))
{ {

View File

@@ -14,13 +14,12 @@ internal static class LocalSetting
/// <summary> /// <summary>
/// 由于 <see cref="Windows.Foundation.Collections.IPropertySet"/> 没有 nullable context, /// 由于 <see cref="Windows.Foundation.Collections.IPropertySet"/> 没有 nullable context,
/// 在处理引用类型时需要格外小心 /// 在处理引用类型时需要格外小心
/// 将值类型的操作与引用类型区分开,可以提升一定的性能
/// </summary> /// </summary>
private static readonly ApplicationDataContainer Container; private static readonly ApplicationDataContainer Container;
static LocalSetting() static LocalSetting()
{ {
Container = ApplicationData.Current.LocalSettings; Container = App.Settings;
} }
/// <summary> /// <summary>
@@ -79,17 +78,11 @@ internal static class LocalSetting
/// <typeparam name="T">设置项的类型</typeparam> /// <typeparam name="T">设置项的类型</typeparam>
/// <param name="key">键</param> /// <param name="key">键</param>
/// <param name="value">值</param> /// <param name="value">值</param>
public static void Set<T>(string key, T? value) /// <returns>设置的值</returns>
where T : class public static T Set<T>(string key, T value)
{ {
try Container.Values[key] = value;
{ return value;
Container.Values[key] = value;
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
} }
/// <summary> /// <summary>
@@ -105,7 +98,7 @@ internal static class LocalSetting
{ {
Container.Values[key] = value; Container.Values[key] = value;
} }
catch (Exception ex) catch (System.Exception ex)
{ {
Debug.WriteLine(ex); Debug.WriteLine(ex);
} }

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using System.Diagnostics;
using System.Runtime;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Validation; 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> /// <param name="parameterName">The name of the parameter to include in any thrown exception.</param>
/// <returns>The value of the parameter.</returns> /// <returns>The value of the parameter.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is <c>null</c>.</exception> /// <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) 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 where T : class // ensures value-types aren't passed to a null checking method
{ {
if (value is null) return value ?? throw new ArgumentNullException(parameterName);
{
throw new ArgumentNullException(parameterName);
}
return value;
} }
/// <summary> /// <summary>
/// Unconditionally throws an <see cref="NotSupportedException"/>. /// Unconditionally throws an <see cref="NotSupportedException"/>.
/// </summary> /// </summary>
/// <returns>Nothing. This method always throws.</returns> /// <returns>Nothing. This method always throws.</returns>
[DebuggerStepThrough]
[DoesNotReturn] [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 throw new NotSupportedException("该行为不应发生,请联系开发者进一步确认");
// 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
}
} }
/// <summary> /// <summary>
@@ -63,26 +39,11 @@ public static class Must
/// </summary> /// </summary>
/// <typeparam name="T">The type that the method should be typed to return (although it never returns anything).</typeparam> /// <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> /// <returns>Nothing. This method always throws.</returns>
[DebuggerStepThrough]
[DoesNotReturn] [DoesNotReturn]
[return: MaybeNull] [return: MaybeNull]
public static T NeverHappen<T>() public static T NeverHappen<T>()
{ {
// Keep these two as separate lines of code, so the debugger can come in during the assert dialog throw new NotSupportedException("该行为不应发生,请联系开发者进一步确认");
// 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] 的方法。
}
} }
/// <summary> /// <summary>
@@ -91,9 +52,8 @@ public static class Must
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <param name="message">取消消息</param> /// <param name="message">取消消息</param>
/// <exception cref="TaskCanceledException">任务被取消</exception> /// <exception cref="TaskCanceledException">任务被取消</exception>
[DebuggerStepThrough]
[SuppressMessage("", "CA1068")] [SuppressMessage("", "CA1068")]
public static void TryThrowOnCanceled(CancellationToken token, string message) public static void ThrowOnCanceled(CancellationToken token, string message)
{ {
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
{ {

View File

@@ -3,6 +3,7 @@
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Core.Logging; using Snap.Hutao.Core.Logging;
using System.IO;
namespace Snap.Hutao.Core; namespace Snap.Hutao.Core;
@@ -35,7 +36,7 @@ internal abstract class WebView2Helper
{ {
version = CoreWebView2Environment.GetAvailableBrowserVersionString(); version = CoreWebView2Environment.GetAvailableBrowserVersionString();
} }
catch (Exception ex) catch (FileNotFoundException ex)
{ {
ILogger<WebView2Helper> logger = Ioc.Default.GetRequiredService<ILogger<WebView2Helper>>(); ILogger<WebView2Helper> logger = Ioc.Default.GetRequiredService<ILogger<WebView2Helper>>();
logger.LogError(EventIds.WebView2EnvironmentException, ex, "WebView2 运行时未安装"); logger.LogError(EventIds.WebView2EnvironmentException, ex, "WebView2 运行时未安装");

View 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);
}
}

View File

@@ -20,9 +20,8 @@ public static class AppActivationArgumentsExtensions
public static bool TryGetProtocolActivatedUri(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out Uri? uri) public static bool TryGetProtocolActivatedUri(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out Uri? uri)
{ {
uri = null; uri = null;
if (activatedEventArgs.Kind == ExtendedActivationKind.Protocol) if (activatedEventArgs.Data is IProtocolActivatedEventArgs protocolArgs)
{ {
IProtocolActivatedEventArgs protocolArgs = (activatedEventArgs.Data as IProtocolActivatedEventArgs)!;
uri = protocolArgs.Uri; uri = protocolArgs.Uri;
return true; return true;
} }

View File

@@ -20,4 +20,3 @@ public static class PackageVersionExtensions
return new Version(packageVersion.Major, packageVersion.Minor, packageVersion.Build, packageVersion.Revision); return new Version(packageVersion.Major, packageVersion.Minor, packageVersion.Build, packageVersion.Revision);
} }
} }

View File

@@ -19,7 +19,7 @@ namespace Snap.Hutao;
/// </summary> /// </summary>
internal static class IocHttpClientConfiguration 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> /// <summary>
/// 添加 <see cref="HttpClient"/> /// 添加 <see cref="HttpClient"/>
@@ -30,7 +30,8 @@ internal static class IocHttpClientConfiguration
{ {
// services // services
services.AddHttpClient<MetadataService>(DefaultConfiguration); services.AddHttpClient<MetadataService>(DefaultConfiguration);
services.AddHttpClient<ImageCache>(DefaultConfiguration).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { MaxConnectionsPerServer = 20 }); services.AddHttpClient<ImageCache>(DefaultConfiguration)
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { MaxConnectionsPerServer = 20 });
// normal clients // normal clients
services.AddHttpClient<AnnouncementClient>(DefaultConfiguration); services.AddHttpClient<AnnouncementClient>(DefaultConfiguration);

View File

@@ -3,11 +3,7 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Snap.Hutao.Context.Database; using Snap.Hutao.Context.Database;
using Snap.Hutao.Control.HostBackdrop; using Snap.Hutao.Core;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Win32;
using WinRT.Interop;
namespace Snap.Hutao; namespace Snap.Hutao;
@@ -18,8 +14,6 @@ namespace Snap.Hutao;
public sealed partial class MainWindow : Window public sealed partial class MainWindow : Window
{ {
private readonly AppDbContext appDbContext; private readonly AppDbContext appDbContext;
private readonly ILogger<MainWindow> logger;
private readonly IntPtr handle;
/// <summary> /// <summary>
/// 构造一个新的主窗体 /// 构造一个新的主窗体
@@ -29,56 +23,13 @@ public sealed partial class MainWindow : Window
public MainWindow(AppDbContext appDbContext, ILogger<MainWindow> logger) public MainWindow(AppDbContext appDbContext, ILogger<MainWindow> logger)
{ {
this.appDbContext = appDbContext; this.appDbContext = appDbContext;
this.logger = logger;
InitializeComponent(); InitializeComponent();
handle = WindowNative.GetWindowHandle(this); _ = new WindowManager(this, TitleBarView.DragableArea);
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");
} }
private void MainWindowClosed(object sender, WindowEventArgs args) private void MainWindowClosed(object sender, WindowEventArgs args)
{ {
SaveWindowRect(handle); // save userdata datebase
// save datebase
int changes = appDbContext.SaveChanges(); int changes = appDbContext.SaveChanges();
Verify.Operation(changes == 0, "存在未经处理的数据库记录更改"); Verify.Operation(changes == 0, "存在未经处理的数据库记录更改");
} }

View 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();
}
}

View File

@@ -1,16 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // 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;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Windows.ApplicationModel.DataTransfer;
namespace Snap.Hutao.Model.Entity; namespace Snap.Hutao.Model.Entity;
@@ -18,15 +10,8 @@ namespace Snap.Hutao.Model.Entity;
/// 用户 /// 用户
/// </summary> /// </summary>
[Table("users")] [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> /// <summary>
/// 内部Id /// 内部Id
/// </summary> /// </summary>
@@ -44,44 +29,6 @@ public class User : Observable
/// </summary> /// </summary>
public string? Cookie { get; set; } 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> /// <summary>
/// 设置用户的选中状态 /// 设置用户的选中状态
/// 同时更新用户选择的角色信息 /// 同时更新用户选择的角色信息
@@ -90,83 +37,6 @@ public class User : Observable
/// <param name="isSelected">是否选中</param> /// <param name="isSelected">是否选中</param>
public static void SetSelectionState(User user, bool isSelected) public static void SetSelectionState(User user, bool isSelected)
{ {
Verify.Operation(!IsNone(user), "尝试设置一个空的用户");
user!.IsSelected = isSelected; 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);
}
} }
} }

View File

@@ -8,6 +8,11 @@ namespace Snap.Hutao.Model.Metadata.Achievement;
/// </summary> /// </summary>
public class AchievementGoal public class AchievementGoal
{ {
/// <summary>
/// Id
/// </summary>
public int Id { get; set; }
/// <summary> /// <summary>
/// 排序顺序 /// 排序顺序
/// </summary> /// </summary>

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色头像转换器
/// </summary>
internal class 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();
}
}

View File

@@ -25,6 +25,12 @@ internal class AvatarNameCardPicConverter : IValueConverter
return new Uri(string.Format(BaseUrl, avatarName)); 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) private static string ReplaceSpecialCaseNaming(string avatarName)
{ {
return avatarName switch return avatarName switch
@@ -33,10 +39,4 @@ internal class AvatarNameCardPicConverter : IValueConverter
_ => avatarName, _ => avatarName,
}; };
} }
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
}
} }

View File

@@ -21,6 +21,7 @@ public class Pair<TKey, TValue>
Value = value; Value = value;
} }
/// <summary>
/// 键 /// 键
/// </summary> /// </summary>
public TKey Key { get; set; } public TKey Key { get; set; }

View File

@@ -23,7 +23,7 @@
</Dependencies> </Dependencies>
<Resources> <Resources>
<Resource Language="x-generate"/> <Resource Language="zh-CN"/>
</Resources> </Resources>
<Applications> <Applications>

View File

@@ -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
{
}

View File

@@ -0,0 +1,9 @@
namespace Snap.Hutao.Service.Achievement;
/// <summary>
/// 成就服务抽象
/// </summary>
internal interface IAchievementService
{
}

View File

@@ -26,47 +26,47 @@ internal interface IMetadataService
/// </summary> /// </summary>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>成就列表</returns> /// <returns>成就列表</returns>
ValueTask<IEnumerable<Achievement>> GetAchievementsAsync(CancellationToken token = default); ValueTask<List<Model.Metadata.Achievement.Achievement>> GetAchievementsAsync(CancellationToken token = default);
/// <summary> /// <summary>
/// 异步获取成就分类列表 /// 异步获取成就分类列表
/// </summary> /// </summary>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>成就分类列表</returns> /// <returns>成就分类列表</returns>
ValueTask<IEnumerable<AchievementGoal>> GetAchievementGoalsAsync(CancellationToken token = default); ValueTask<List<AchievementGoal>> GetAchievementGoalsAsync(CancellationToken token = default);
/// <summary> /// <summary>
/// 异步获取角色列表 /// 异步获取角色列表
/// </summary> /// </summary>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>角色列表</returns> /// <returns>角色列表</returns>
ValueTask<IEnumerable<Avatar>> GetAvatarsAsync(CancellationToken token = default); ValueTask<List<Avatar>> GetAvatarsAsync(CancellationToken token = default);
/// <summary> /// <summary>
/// 异步获取圣遗物列表 /// 异步获取圣遗物列表
/// </summary> /// </summary>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>圣遗物列表</returns> /// <returns>圣遗物列表</returns>
ValueTask<IEnumerable<Reliquary>> GetReliquariesAsync(CancellationToken token = default); ValueTask<List<Reliquary>> GetReliquariesAsync(CancellationToken token = default);
/// <summary> /// <summary>
/// 异步获取圣遗物强化属性列表 /// 异步获取圣遗物强化属性列表
/// </summary> /// </summary>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>圣遗物强化属性列表</returns> /// <returns>圣遗物强化属性列表</returns>
ValueTask<IEnumerable<ReliquaryAffix>> GetReliquaryAffixesAsync(CancellationToken token = default); ValueTask<List<ReliquaryAffix>> GetReliquaryAffixesAsync(CancellationToken token = default);
/// <summary> /// <summary>
/// 异步获取圣遗物主属性强化属性列表 /// 异步获取圣遗物主属性强化属性列表
/// </summary> /// </summary>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>圣遗物强化属性列表</returns> /// <returns>圣遗物强化属性列表</returns>
ValueTask<IEnumerable<ReliquaryAffixBase>> GetReliquaryMainAffixesAsync(CancellationToken token = default); ValueTask<List<ReliquaryAffixBase>> GetReliquaryMainAffixesAsync(CancellationToken token = default);
/// <summary> /// <summary>
/// 异步获取武器列表 /// 异步获取武器列表
/// </summary> /// </summary>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>武器列表</returns> /// <returns>武器列表</returns>
ValueTask<IEnumerable<Weapon>> GetWeaponsAsync(CancellationToken token = default); ValueTask<List<Weapon>> GetWeaponsAsync(CancellationToken token = default);
} }

View File

@@ -91,45 +91,45 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
} }
/// <inheritdoc/> /// <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/> /// <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/> /// <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/> /// <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/> /// <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/> /// <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/> /// <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) private async Task<bool> TryUpdateMetadataAsync(CancellationToken token = default)

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Binding;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
@@ -13,25 +13,24 @@ namespace Snap.Hutao.Service.Abstraction;
public interface IUserService public interface IUserService
{ {
/// <summary> /// <summary>
/// 获取当前用户信息 /// 获取或设置当前用户
/// </summary> /// </summary>
User? CurrentUser { get; set; } User? CurrentUser { get; set; }
/// <summary> /// <summary>
/// 异步获取用户信息枚举 /// 异步获取同步的用户信息集合
/// 每个用户信息都应准备完成 /// 对集合的操作应通过服务抽象完成
/// 此操作不能取消 /// 此操作不能取消
/// </summary> /// </summary>
/// <param name="removeCommand">移除用户命令</param>
/// <returns>准备完成的用户信息枚举</returns> /// <returns>准备完成的用户信息枚举</returns>
Task<ObservableCollection<User>> GetInitializedUsersAsync(); Task<ObservableCollection<User>> GetUserCollectionAsync();
/// <summary> /// <summary>
/// 异步添加用户 /// 异步添加用户
/// 通常用户是未初始化的 /// 通常用户是未初始化的
/// </summary> /// </summary>
/// <param name="user">待添加的用户</param> /// <param name="user">待添加的用户</param>
/// <param name="uid">用户的米游社UID</param> /// <param name="uid">用户的米游社UID,用于检查是否包含重复的用户</param>
/// <returns>用户初始化是否成功</returns> /// <returns>用户初始化是否成功</returns>
Task<UserAddResult> TryAddUserAsync(User user, string uid); Task<UserAddResult> TryAddUserAsync(User user, string uid);
@@ -48,4 +47,11 @@ public interface IUserService
/// <param name="cookie">cookie的字符串形式</param> /// <param name="cookie">cookie的字符串形式</param>
/// <returns>包含cookie信息的字典</returns> /// <returns>包含cookie信息的字典</returns>
IDictionary<string, string> ParseCookie(string cookie); IDictionary<string, string> ParseCookie(string cookie);
/// <summary>
/// 创建一个新的绑定用户
/// </summary>
/// <param name="cookie">cookie的字符串形式</param>
/// <returns>新的绑定用户</returns>
Task<User?> CreateUserAsync(string cookie);
} }

View File

@@ -22,9 +22,4 @@ public enum UserAddResult
/// 已经存在该用户 /// 已经存在该用户
/// </summary> /// </summary>
AlreadyExists, AlreadyExists,
/// <summary>
/// 初始化用户失败
/// </summary>
InitializeFailed,
} }

View File

@@ -1,9 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Context.Database; using Snap.Hutao.Context.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab.Bbs.User; using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding; using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
@@ -15,16 +13,17 @@ namespace Snap.Hutao.Service;
/// <summary> /// <summary>
/// 用户服务 /// 用户服务
/// 主要负责将用户数据与数据库同步
/// </summary> /// </summary>
[Injection(InjectAs.Transient, typeof(IUserService))] [Injection(InjectAs.Singleton, typeof(IUserService))]
internal class UserService : IUserService internal class UserService : IUserService
{ {
private readonly AppDbContext appDbContext; private readonly AppDbContext appDbContext;
private readonly UserClient userClient; private readonly UserClient userClient;
private readonly UserGameRoleClient userGameRoleClient; private readonly UserGameRoleClient userGameRoleClient;
private User? currentUser; private Model.Binding.User? currentUser;
private ObservableCollection<User>? cachedUsers = null; private ObservableCollection<Model.Binding.User>? userCollection = null;
/// <summary> /// <summary>
/// 构造一个新的用户服务 /// 构造一个新的用户服务
@@ -40,18 +39,18 @@ internal class UserService : IUserService
} }
/// <inheritdoc/> /// <inheritdoc/>
public User? CurrentUser public Model.Binding.User? CurrentUser
{ {
get => currentUser; get => currentUser;
set set
{ {
// only update when not processing a deletion // 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); currentUser.IsSelected = false;
appDbContext.Users.Update(currentUser); appDbContext.Users.Update(currentUser.Entity);
appDbContext.SaveChanges(); appDbContext.SaveChanges();
} }
} }
@@ -59,91 +58,98 @@ internal class UserService : IUserService
// 当删除到无用户时也能正常反应状态 // 当删除到无用户时也能正常反应状态
currentUser = value; currentUser = value;
if (!User.IsNone(currentUser)) if (currentUser != null)
{ {
User.SetSelectionState(currentUser, true); currentUser.IsSelected = true;
appDbContext.Users.Update(currentUser); appDbContext.Users.Update(currentUser.Entity);
appDbContext.SaveChanges(); appDbContext.SaveChanges();
} }
} }
} }
/// <inheritdoc/> /// <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 // 查找是否有相同的uid
User? userWithSameUid = cachedUsers if (userCollection.SingleOrDefault(u => u.UserInfo!.Uid == uid) is Model.Binding.User userWithSameUid)
.SingleOrDefault(u => u.UserInfo!.Uid == uid);
if (userWithSameUid != null)
{ {
// Prevent users from adding same cookie. // Prevent users from adding a completely same cookie.
if (userWithSameUid.Cookie == newUser.Cookie) if (userWithSameUid.Cookie == newUser.Cookie)
{ {
return UserAddResult.AlreadyExists; return UserAddResult.AlreadyExists;
} }
else else
{ {
// Try update user here. // Update user cookie here.
userWithSameUid.Cookie = newUser.Cookie; userWithSameUid.Cookie = newUser.Cookie;
appDbContext.Users.Update(userWithSameUid); appDbContext.Users.Update(userWithSameUid.Entity);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
await appDbContext
.SaveChangesAsync()
.ConfigureAwait(false);
return UserAddResult.Updated; return UserAddResult.Updated;
} }
} }
else
// must continue on the caller thread.
if (await newUser.InitializeAsync(userClient, userGameRoleClient))
{ {
appDbContext.Users.Add(newUser); Verify.Operation(newUser.IsInitialized, "该用户尚未初始化");
await appDbContext // Sync database
.SaveChangesAsync() appDbContext.Users.Add(newUser.Entity);
.ConfigureAwait(false); await appDbContext.SaveChangesAsync().ConfigureAwait(false);
// Sync cache
userCollection.Add(newUser);
return UserAddResult.Added; return UserAddResult.Added;
} }
return UserAddResult.InitializeFailed;
} }
/// <inheritdoc/> /// <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(); return appDbContext.SaveChangesAsync();
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<ObservableCollection<User>> GetInitializedUsersAsync() public async Task<ObservableCollection<Model.Binding.User>> GetUserCollectionAsync()
{ {
if (cachedUsers == null) if (userCollection == null)
{ {
await appDbContext.Users List<Model.Binding.User> users = new();
.LoadAsync()
.ConfigureAwait(false);
cachedUsers = appDbContext.Users.Local.ToObservableCollection(); foreach (Model.Entity.User user in appDbContext.Users)
foreach (User user in cachedUsers)
{ {
await user Model.Binding.User? initialized = await Model.Binding.User
.InitializeAsync(userClient, userGameRoleClient) .CreateAsync(user, userClient, userGameRoleClient)
.ConfigureAwait(false); .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 CurrentUser = users.SingleOrDefault(user => user.IsSelected);
.SingleOrDefaultAsync(user => user.IsSelected)
.ConfigureAwait(false); 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/> /// <inheritdoc/>

View File

@@ -31,6 +31,9 @@
<ItemsControl <ItemsControl
x:Name="DetailsHost" x:Name="DetailsHost"
VerticalAlignment="Top"> VerticalAlignment="Top">
<ItemsControl.ItemContainerTransitions>
<ContentThemeTransition/>
</ItemsControl.ItemContainerTransitions>
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<sc:Setting Margin="0,2,0,0" Header="{Binding Description}" Padding="12,0"> <sc:Setting Margin="0,2,0,0" Header="{Binding Description}" Padding="12,0">

View File

@@ -44,9 +44,7 @@
<Frame x:Name="ContentFrame"> <Frame x:Name="ContentFrame">
<Frame.ContentTransitions> <Frame.ContentTransitions>
<TransitionCollection> <NavigationThemeTransition/>
<NavigationThemeTransition/>
</TransitionCollection>
</Frame.ContentTransitions> </Frame.ContentTransitions>
</Frame> </Frame>
</NavigationView> </NavigationView>

View File

@@ -5,28 +5,116 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mxi="using:Microsoft.Xaml.Interactivity" xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:sc="using:SettingsUI.Controls"
xmlns:shcb="using:Snap.Hutao.Control.Behavior" xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shcc="using:Snap.Hutao.Control.Cancellable" 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" 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> <mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/> <shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors> </mxi:Interaction.Behaviors>
<Grid> <Grid>
<ScrollViewer> <SplitView
<ItemsControl IsPaneOpen="True"
Margin="12,0,16,12" DisplayMode="Inline"
ItemsSource="{Binding AchievementsView}"> OpenPaneLength="252">
<ItemsControl.ItemTemplate> <SplitView.PaneBackground>
<DataTemplate> <SolidColorBrush Color="{ThemeResource CardBackgroundFillColorSecondary}"/>
<sc:Setting </SplitView.PaneBackground>
Margin="0,12,0,0" <SplitView.Pane>
Header="{Binding Title}" <ListView
Description="{Binding Description}"/> SelectionMode="Single"
</DataTemplate> SelectedItem="{Binding SelectedAchievementGoal,Mode=TwoWay}"
</ItemsControl.ItemTemplate> ItemsSource="{Binding AchievementGoals}">
</ItemsControl> <ListView.ItemTemplate>
</ScrollViewer> <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> </Grid>
</shcc:CancellablePage> </shcc:CancellablePage>

View File

@@ -16,17 +16,21 @@ namespace Snap.Hutao.View.Page;
/// </summary> /// </summary>
public sealed partial class AnnouncementContentPage : Microsoft.UI.Xaml.Controls.Page 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 LightColor1 = "color:rgba(255,255,255,1)";
private const string LightColor2 = "color:rgba(238,238,238,1)"; private const string LightColor2 = "color:rgba(238,238,238,1)";
private const string LightColor3 = "color:rgba(204,204,204,1)"; private const string LightColor3 = "color:rgba(204,204,204,1)";
private const string LightColor4 = "color:rgba(198,196,191,1)"; private const string LightColor4 = "color:rgba(198,196,191,1)";
private const string LightColor5 = "color:rgba(170,170,170,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 DarkColor1 = "color:rgba(0,0,0,1)";
private const string DarkColor2 = "color:rgba(17,17,17,1)"; private const string DarkColor2 = "color:rgba(17,17,17,1)";
private const string DarkColor3 = "color:rgba(51,51,51,1)"; private const string DarkColor3 = "color:rgba(51,51,51,1)";
private const string DarkColor4 = "color:rgba(57,59,64,1)"; private const string DarkColor4 = "color:rgba(57,59,64,1)";
private const string DarkColor5 = "color:rgba(85,85,85,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. // support click open browser.
private const string MihoyoSDKDefinition = private const string MihoyoSDKDefinition =
@@ -76,7 +80,8 @@ openInWebview: function(url){ location.href = url }}";
.Replace(DarkColor5, LightColor5) .Replace(DarkColor5, LightColor5)
.Replace(DarkColor4, LightColor4) .Replace(DarkColor4, LightColor4)
.Replace(DarkColor3, LightColor3) .Replace(DarkColor3, LightColor3)
.Replace(DarkColor2, LightColor2); .Replace(DarkColor2, LightColor2)
.Replace(DarkAccentColor1, LightAccentColor);
} }
// wrap a default color body around // wrap a default color body around

View File

@@ -10,7 +10,7 @@
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
d:DataContext="{d:DesignInstance shv:SettingViewModel}"> d:DataContext="{d:DesignInstance shv:SettingViewModel}">
<Page.Resources> <Page.Resources>
<Style TargetType="Button" BasedOn="{StaticResource DefaultButtonStyle}"> <Style TargetType="Button" BasedOn="{StaticResource SettingButtonStyle}">
<Setter Property="MinWidth" Value="160"/> <Setter Property="MinWidth" Value="160"/>
</Style> </Style>
</Page.Resources> </Page.Resources>

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.WinUI.UI; using CommunityToolkit.WinUI.UI;
using Microsoft.VisualStudio.Threading;
using Snap.Hutao.Control.Cancellable; using Snap.Hutao.Control.Cancellable;
using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Metadata.Achievement; using Snap.Hutao.Model.Metadata.Achievement;
@@ -20,7 +19,9 @@ namespace Snap.Hutao.ViewModel;
internal class AchievementViewModel : ObservableObject, ISupportCancellation internal class AchievementViewModel : ObservableObject, ISupportCancellation
{ {
private readonly IMetadataService metadataService; private readonly IMetadataService metadataService;
private AdvancedCollectionView? achievementsView; private AdvancedCollectionView? achievements;
private IList<AchievementGoal>? achievementGoals;
private AchievementGoal? selectedAchievementGoal;
/// <summary> /// <summary>
/// 构造一个新的成就视图模型 /// 构造一个新的成就视图模型
@@ -39,10 +40,32 @@ internal class AchievementViewModel : ObservableObject, ISupportCancellation
/// <summary> /// <summary>
/// 成就视图 /// 成就视图
/// </summary> /// </summary>
public AdvancedCollectionView? AchievementsView public AdvancedCollectionView? Achievements
{ {
get => achievementsView; get => achievements;
set => SetProperty(ref achievementsView, value); 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> /// <summary>
@@ -50,17 +73,22 @@ internal class AchievementViewModel : ObservableObject, ISupportCancellation
/// </summary> /// </summary>
public ICommand OpenUICommand { get; } 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)) Achievements = new(await metadataService.GetAchievementsAsync(CancellationToken), true);
{ AchievementGoals = await metadataService.GetAchievementGoalsAsync(CancellationToken);
IEnumerable<Achievement> achievements = await metadataService.GetAchievementsAsync(combined.Token); }
}
// TODO private void OnGoalChanged(AchievementGoal? goal)
AchievementsView = new(achievements.ToList()); {
} if (Achievements != null)
{
Achievements.Filter = goal != null
? ((object o) => o is Achievement achi && achi.Goal == goal.Id)
: ((object o) => true);
} }
} }
} }

View File

@@ -41,7 +41,7 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
private Task OpenCacheFolderAsync(CancellationToken token) 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) private Task OpenDataFolderAsync(CancellationToken token)

View File

@@ -3,9 +3,10 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Threading; using Snap.Hutao.Core.Threading;
using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Binding;
using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.View.Dialog; using Snap.Hutao.View.Dialog;
using System.Collections.Generic; using System.Collections.Generic;
@@ -27,7 +28,7 @@ internal class UserViewModel : ObservableObject
private readonly IInfoBarService infoBarService; private readonly IInfoBarService infoBarService;
private User? selectedUser; private User? selectedUser;
private ObservableCollection<User>? userInfos; private ObservableCollection<User>? users;
/// <summary> /// <summary>
/// 构造一个新的用户视图模型 /// 构造一个新的用户视图模型
@@ -64,7 +65,7 @@ internal class UserViewModel : ObservableObject
/// <summary> /// <summary>
/// 用户信息集合 /// 用户信息集合
/// </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> /// <summary>
/// 打开界面命令 /// 打开界面命令
@@ -90,20 +91,20 @@ internal class UserViewModel : ObservableObject
{ {
int validFlag = 4; 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) foreach ((string key, string value) in map)
{ {
if (key == AccountIdKey || key == "cookie_token" || key == "ltoken" || key == "ltuid") if (key == AccountIdKey || key == "cookie_token" || key == "ltoken" || key == "ltuid")
{ {
validFlag--; validFlag--;
filteredCookie.Add(key, value); filter.Add(key, value);
} }
} }
if (validFlag == 0) if (validFlag == 0)
{ {
filteredCookie = filter;
return true; return true;
} }
else else
@@ -115,41 +116,43 @@ internal class UserViewModel : ObservableObject
private async Task OpenUIAsync() private async Task OpenUIAsync()
{ {
Users = await userService.GetInitializedUsersAsync(); Users = await userService.GetUserCollectionAsync();
SelectedUser = userService.CurrentUser; SelectedUser = userService.CurrentUser;
} }
private async Task AddUserAsync() 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(); Result<bool, string> result = await new UserDialog(mainWindow).GetInputCookieAsync();
// user confirms the input // User confirms the input
if (result.IsOk) if (result.IsOk)
{ {
IDictionary<string, string> cookieMap = userService.ParseCookie(result.Value); if (TryValidateCookie(userService.ParseCookie(result.Value), out IDictionary<string, string>? filteredCookie))
if (TryValidateCookie(cookieMap, out IDictionary<string, string>? filteredCookie))
{ {
string simplifiedCookie = string.Join(';', filteredCookie.Select(kvp => $"{kvp.Key}={kvp.Value}")); 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: switch (await userService.TryAddUserAsync(user, filteredCookie[AccountIdKey]))
infoBarService.Success($"用户 [{user.UserInfo!.Nickname}] 添加成功"); {
break; case UserAddResult.Added:
case UserAddResult.Updated: infoBarService.Success($"用户 [{user.UserInfo!.Nickname}] 添加成功");
infoBarService.Success($"用户 [{user.UserInfo!.Nickname}] 更新成功"); break;
break; case UserAddResult.Updated:
case UserAddResult.AlreadyExists: infoBarService.Success($"用户 [{user.UserInfo!.Nickname}] 更新成功");
infoBarService.Information($"用户 [{user.UserInfo!.Nickname}] 已经存在"); break;
break; case UserAddResult.AlreadyExists:
case UserAddResult.InitializeFailed: infoBarService.Information($"用户 [{user.UserInfo!.Nickname}] 已经存在");
infoBarService.Warning("此 Cookie 无法获取用户信息,请重新输入"); break;
break; default:
default: throw Must.NeverHappen();
throw Must.NeverHappen(); }
}
else
{
infoBarService.Warning("此 Cookie 无法获取用户信息,请重新输入");
} }
} }
else else
@@ -161,19 +164,14 @@ internal class UserViewModel : ObservableObject
private async Task RemoveUserAsync(User? user) private async Task RemoveUserAsync(User? user)
{ {
if (!User.IsNone(user)) Verify.Operation(user != null, "待删除的用户不应为 null");
{ await userService.RemoveUserAsync(user);
await userService.RemoveUserAsync(user); infoBarService.Success($"用户 [{user.UserInfo?.Nickname}] 成功移除");
infoBarService.Success($"用户 [{user.UserInfo!.Nickname}] 成功移除");
}
} }
private void CopyCookie(User? user) private void CopyCookie(User? user)
{ {
if (User.IsNone(user)) Verify.Operation(user != null, "待复制 Cookie 的用户不应为 null");
{
return;
}
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>(); IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
try try

View File

@@ -20,6 +20,8 @@ namespace Snap.Hutao.ViewModel;
internal class WikiAvatarViewModel : ObservableObject internal class WikiAvatarViewModel : ObservableObject
{ {
private readonly IMetadataService metadataService; private readonly IMetadataService metadataService;
// filters
private readonly List<Selectable<string>> filterElementInfos; private readonly List<Selectable<string>> filterElementInfos;
private readonly List<Selectable<Pair<string, string>>> filterAssociationInfos; private readonly List<Selectable<Pair<string, string>>> filterAssociationInfos;
private readonly List<Selectable<Pair<string, WeaponType>>> filterWeaponTypeInfos; private readonly List<Selectable<Pair<string, WeaponType>>> filterWeaponTypeInfos;
@@ -129,12 +131,12 @@ internal class WikiAvatarViewModel : ObservableObject
{ {
if (await metadataService.InitializeAsync()) if (await metadataService.InitializeAsync())
{ {
IEnumerable<Avatar>? avatars = await metadataService.GetAvatarsAsync(); IList<Avatar> avatars = await metadataService.GetAvatarsAsync();
IOrderedEnumerable<Avatar> sorted = avatars IOrderedEnumerable<Avatar> sorted = avatars
.OrderBy(avatar => avatar.BeginTime) .OrderBy(avatar => avatar.BeginTime)
.ThenBy(avatar => avatar.Sort); .ThenBy(avatar => avatar.Sort);
Avatars = new AdvancedCollectionView(new List<Avatar>(sorted), true); Avatars = new AdvancedCollectionView(sorted.ToList(), true);
Avatars.MoveCurrentToFirst(); Avatars.MoveCurrentToFirst();
} }
} }
@@ -175,7 +177,10 @@ internal class WikiAvatarViewModel : ObservableObject
&& targeQualities.Contains(avatar.Quality) && targeQualities.Contains(avatar.Quality)
&& targetBodies.Contains(avatar.Body); && targetBodies.Contains(avatar.Body);
Avatars.MoveCurrentToFirst(); if (!Avatars.Contains(Selected))
{
Avatars.MoveCurrentToFirst();
}
} }
} }
} }

View File

@@ -35,7 +35,7 @@ internal class UserClient
/// <param name="user">用户</param> /// <param name="user">用户</param>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>详细信息</returns> /// <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 Response<UserFullInfoWrapper>? resp = await httpClient
.UsingDynamicSecret() .UsingDynamicSecret()

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Binding;
using System.Net.Http; using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab; namespace Snap.Hutao.Web.Hoyolab;
@@ -19,11 +19,7 @@ internal static class HttpClientCookieExtensions
/// <returns>客户端</returns> /// <returns>客户端</returns>
internal static HttpClient SetUser(this HttpClient httpClient, User user) 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; return httpClient;
} }
} }

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Extension; using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding;
using Snap.Hutao.Web.Response; using Snap.Hutao.Web.Response;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
@@ -33,7 +34,7 @@ internal class UserGameRoleClient
/// <param name="user">用户</param> /// <param name="user">用户</param>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>用户角色信息</returns> /// <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 Response<ListWrapper<UserGameRole>>? resp = await httpClient
.SetUser(user) .SetUser(user)

View File

@@ -2,7 +2,7 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Extension; 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.DynamicSecret;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar; using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using Snap.Hutao.Web.Response; using Snap.Hutao.Web.Response;

View File

@@ -294,7 +294,7 @@ internal class HutaoClient : ISupportAsyncInitialization
/// <param name="user">用户</param> /// <param name="user">用户</param>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
/// <returns>玩家记录</returns> /// <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 PlayerInfo? playerInfo = await gameRecordClient
.GetPlayerInfoAsync(user, token) .GetPlayerInfoAsync(user, token)