From 641a731a4d9e0d51e394601eb49f5aae92b38981 Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Tue, 16 Aug 2022 14:29:33 +0800 Subject: [PATCH] Achievement functional --- src/Snap.Hutao/SettingsUI/SettingsUI.csproj | 2 +- src/Snap.Hutao/Snap.Hutao/App.xaml.cs | 33 +- .../Context/Database/AppDbContext.cs | 14 +- .../Behavior/InvokeCommandOnLoadedBehavior.cs | 2 +- .../InvokeCommandOnUnloadedBehavior.cs | 36 ++ .../Snap.Hutao/Control/ContentDialog2.cs | 26 ++ .../Core/LifeCycle/ExceptionRecorder.cs | 59 +++ .../Snap.Hutao/Core/Logging/EventIds.cs | 5 + .../Extension/DateTimeOffsetExtensions.cs | 26 ++ .../Snap.Hutao/Extension/TupleExtensions.cs | 2 +- src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs | 13 +- .../AchievementArchiveChangedMessage.cs | 22 ++ .../Message/MainWindowClosedMessage.cs | 13 + .../Snap.Hutao/Message/UserChangedMessage.cs | 22 ++ .../Snap.Hutao/Message/ValueChangedMessage.cs | 33 ++ .../20220813040006_AddAchievement.Designer.cs | 90 +++++ .../20220813040006_AddAchievement.cs | 45 +++ ...15133601_AddAchievementArchive.Designer.cs | 111 ++++++ .../20220815133601_AddAchievementArchive.cs | 87 +++++ .../Migrations/AppDbContextModelSnapshot.cs | 61 ++- .../Snap.Hutao/Model/Binding/Achievement.cs | 63 +++ .../Snap.Hutao/Model/Entity/Achievement.cs | 118 ++++++ .../Model/Entity/AchievementArchive.cs | 41 ++ .../Snap.Hutao/Model/Entity/User.cs | 12 +- .../Model/InterChange/Achievement/UIAF.cs | 24 ++ .../Model/InterChange/Achievement/UIAFInfo.cs | 44 +++ .../Model/InterChange/Achievement/UIAFItem.cs | 33 ++ .../Model/Intrinsic/AchievementInfoStatus.cs | 31 ++ .../Snap.Hutao/Model/Intrinsic/ElementType.cs | 2 +- src/Snap.Hutao/Snap.Hutao/Model/Observable.cs | 8 +- .../Snap.Hutao/Package.appxmanifest | 2 +- src/Snap.Hutao/Snap.Hutao/Program.cs | 1 + .../Resource/Icon/UI_ItemIcon_201.png | Bin 0 -> 29654 bytes .../Service/Achievement/AchievementService.cs | 362 +++++++++++++++++- .../Service/Achievement/ArchiveAddResult.cs | 25 ++ .../Achievement/IAchievementService.cs | 59 ++- .../Service/Achievement/ImportOption.cs | 25 ++ .../Service/Achievement/ImportResult.cs | 38 ++ .../Snap.Hutao/Service/InfoBarService.cs | 1 - .../Snap.Hutao/Service/User/UserService.cs | 21 +- src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 37 +- .../AchievementArchiveCreateDialog.xaml | 22 ++ .../AchievementArchiveCreateDialog.xaml.cs | 36 ++ .../View/Dialog/AchievementImportDialog.xaml | 78 ++++ .../Dialog/AchievementImportDialog.xaml.cs | 52 +++ .../Snap.Hutao/View/Page/AchievementPage.xaml | 74 +++- .../View/Page/AchievementPage.xaml.cs | 9 + .../View/Page/AnnouncementContentPage.xaml.cs | 1 - src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml | 3 +- .../ViewModel/AchievementViewModel.cs | 315 ++++++++++++++- .../Snap.Hutao/ViewModel/UserViewModel.cs | 7 +- .../HttpClientDynamicSecretExtensions.cs | 2 - .../Takumi/Event/BbsSignReward/Reward.cs | 3 + .../Event/BbsSignReward/SignInResult.cs | 11 +- 54 files changed, 2169 insertions(+), 93 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/Behavior/InvokeCommandOnUnloadedBehavior.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/ContentDialog2.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/ExceptionRecorder.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Extension/DateTimeOffsetExtensions.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Message/AchievementArchiveChangedMessage.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Message/MainWindowClosedMessage.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Message/UserChangedMessage.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Message/ValueChangedMessage.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Migrations/20220813040006_AddAchievement.Designer.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Migrations/20220813040006_AddAchievement.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Migrations/20220815133601_AddAchievementArchive.Designer.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Migrations/20220815133601_AddAchievementArchive.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Binding/Achievement.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Entity/Achievement.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Entity/AchievementArchive.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAF.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFItem.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/AchievementInfoStatus.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ItemIcon_201.png create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Achievement/ArchiveAddResult.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportOption.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportResult.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml.cs diff --git a/src/Snap.Hutao/SettingsUI/SettingsUI.csproj b/src/Snap.Hutao/SettingsUI/SettingsUI.csproj index 13a4b4c6..fc2ec643 100644 --- a/src/Snap.Hutao/SettingsUI/SettingsUI.csproj +++ b/src/Snap.Hutao/SettingsUI/SettingsUI.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs index 6321b25d..fcd9277a 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs @@ -4,8 +4,8 @@ using CommunityToolkit.Mvvm.Messaging; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; -using Microsoft.VisualStudio.Threading; using Microsoft.Windows.AppLifecycle; +using Snap.Hutao.Core.LifeCycle; using Snap.Hutao.Core.Logging; using Snap.Hutao.Extension; using Snap.Hutao.Service.Abstraction; @@ -23,6 +23,7 @@ public partial class App : Application { private static Window? window; private readonly ILogger logger; + private readonly ExceptionRecorder exceptionRecorder; /// /// Initializes the singleton application object. @@ -37,9 +38,7 @@ public partial class App : Application // so we can use Ioc here. logger = Ioc.Default.GetRequiredService>(); - UnhandledException += AppUnhandledException; - DebugSettings.BindingFailed += XamlBindingFailed; - TaskScheduler.UnobservedTaskException += TaskSchedulerUnobservedTaskException; + exceptionRecorder = new(this, logger); } /// @@ -74,13 +73,11 @@ public partial class App : Application if (firstInstance.IsCurrent) { + OnActivated(firstInstance, activatedEventArgs); firstInstance.Activated += OnActivated; - Window = Ioc.Default.GetRequiredService(); logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", CacheFolder.Path); - OnActivated(firstInstance, activatedEventArgs); - Ioc.Default .GetRequiredService() .ImplictAs()? @@ -106,10 +103,10 @@ public partial class App : Application .AddMemoryCache() // Hutao extensions + .AddJsonSerializerOptions() + .AddDatebase() .AddInjections() .AddHttpClients() - .AddDatebase() - .AddJsonSerializerOptions() // Discrete services .AddSingleton(WeakReferenceMessenger.Default) @@ -123,9 +120,10 @@ public partial class App : Application [SuppressMessage("", "VSTHRD100")] private async void OnActivated(object? sender, AppActivationArguments args) { + Window = Ioc.Default.GetRequiredService(); + IInfoBarService infoBarService = Ioc.Default.GetRequiredService(); await infoBarService.WaitInitializationAsync(); - infoBarService.Information("OnActivated"); if (args.Kind == ExtendedActivationKind.Protocol) { @@ -135,19 +133,4 @@ public partial class App : Application } } } - - 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); - } - - private void TaskSchedulerUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) - { - logger.LogCritical(EventIds.UnobservedTaskException, "异步任务执行异常: {message}", e.Exception); - } } diff --git a/src/Snap.Hutao/Snap.Hutao/Context/Database/AppDbContext.cs b/src/Snap.Hutao/Snap.Hutao/Context/Database/AppDbContext.cs index 6d9b092d..ea6e7dbe 100644 --- a/src/Snap.Hutao/Snap.Hutao/Context/Database/AppDbContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Context/Database/AppDbContext.cs @@ -21,15 +21,25 @@ public class AppDbContext : DbContext } /// - /// 设置项 + /// 设置 /// public DbSet Settings { get; set; } = default!; /// - /// 用户表 + /// 用户 /// public DbSet Users { get; set; } = default!; + /// + /// 成就 + /// + public DbSet Achievements { get; set; } = default!; + + /// + /// 成就存档 + /// + public DbSet AchievementArchives { get; set; } = default!; + /// /// 构造一个临时的应用程序数据库上下文 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Behavior/InvokeCommandOnLoadedBehavior.cs b/src/Snap.Hutao/Snap.Hutao/Control/Behavior/InvokeCommandOnLoadedBehavior.cs index d5520122..9770f84e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Behavior/InvokeCommandOnLoadedBehavior.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Behavior/InvokeCommandOnLoadedBehavior.cs @@ -44,4 +44,4 @@ internal class InvokeCommandOnLoadedBehavior : BehaviorBase base.OnAssociatedObjectLoaded(); } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Behavior/InvokeCommandOnUnloadedBehavior.cs b/src/Snap.Hutao/Snap.Hutao/Control/Behavior/InvokeCommandOnUnloadedBehavior.cs new file mode 100644 index 00000000..8fc192f3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Behavior/InvokeCommandOnUnloadedBehavior.cs @@ -0,0 +1,36 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.WinUI.UI.Behaviors; +using Microsoft.UI.Xaml; +using Snap.Hutao.Core; + +namespace Snap.Hutao.Control.Behavior; + +/// +/// 在元素卸载完成后执行命令的行为 +/// +internal class InvokeCommandOnUnloadedBehavior : BehaviorBase +{ + private static readonly DependencyProperty CommandProperty = Property.Depend(nameof(Command)); + + /// + /// 待执行的命令 + /// + public ICommand Command + { + get => (ICommand)GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + /// + protected override void OnDetaching() + { + if (Command != null && Command.CanExecute(null)) + { + Command.Execute(null); + } + + base.OnDetaching(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/ContentDialog2.cs b/src/Snap.Hutao/Snap.Hutao/Control/ContentDialog2.cs new file mode 100644 index 00000000..ce293009 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/ContentDialog2.cs @@ -0,0 +1,26 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Xaml.Interactivity; +using Snap.Hutao.Control.Behavior; + +namespace Snap.Hutao.Control; + +/// +/// 继承自 实现了某些便捷功能 +/// +internal class ContentDialog2 : ContentDialog +{ + /// + /// 构造一个新的对话框 + /// + /// 窗口 + public ContentDialog2(Window window) + { + DefaultStyleKey = typeof(ContentDialog); + XamlRoot = window.Content.XamlRoot; + Interaction.SetBehaviors(this, new() { new ContentDialogBehavior() }); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/ExceptionRecorder.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/ExceptionRecorder.cs new file mode 100644 index 00000000..859e5258 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/ExceptionRecorder.cs @@ -0,0 +1,59 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Snap.Hutao.Core.Logging; + +namespace Snap.Hutao.Core.LifeCycle; + +/// +/// 异常记录器 +/// +internal class ExceptionRecorder +{ + private readonly ILogger logger; + + /// + /// 构造一个新的异常记录器 + /// + /// 应用程序 + /// 日志器 + public ExceptionRecorder(Application application, ILogger logger) + { + this.logger = logger; + + application.UnhandledException += OnAppUnhandledException; + application.DebugSettings.BindingFailed += OnXamlBindingFailed; + TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; + } + + /// + /// 当应用程序未经处理的异常引发时调用 + /// + /// 实例 + /// 事件参数 + public void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) + { + logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常: [HResult:{code}]", e.Exception.HResult); + } + + /// + /// Xaml 绑定失败时触发 + /// + /// 实例 + /// 事件参数 + public void OnXamlBindingFailed(object? sender, BindingFailedEventArgs e) + { + logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message); + } + + /// + /// 异步命令异常时触发 + /// + /// 实例 + /// 事件参数 + public void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) + { + logger.LogCritical(EventIds.UnobservedTaskException, "异步任务执行异常: {message}", e.Exception); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs index 29bbf2bb..19167e93 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs @@ -73,6 +73,11 @@ internal static class EventIds /// public static readonly EventId FileCaching = 100120; + /// + /// 文件缓存 + /// + public static readonly EventId Achievement = 100130; + // 杂项 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/DateTimeOffsetExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Extension/DateTimeOffsetExtensions.cs new file mode 100644 index 00000000..8926ac5d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Extension/DateTimeOffsetExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Extension; + +/// +/// 扩展 +/// +public static class DateTimeOffsetExtensions +{ + /// + /// Converts the current to a that represents the local time. + /// + /// 时间偏移 + /// 保留主时间部分 + /// A that represents the local time. + public static DateTimeOffset ToLocalTime(this DateTimeOffset dateTimeOffset, bool keepTicks) + { + if (keepTicks) + { + dateTimeOffset += TimeZoneInfo.Local.GetUtcOffset(DateTimeOffset.Now).Negate(); + } + + return dateTimeOffset.ToLocalTime(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/TupleExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Extension/TupleExtensions.cs index 84a498bb..ae8d4066 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/TupleExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/TupleExtensions.cs @@ -22,4 +22,4 @@ public static class TupleExtensions { return new Dictionary(1) { { tuple.Key, tuple.Value } }; } -} \ No newline at end of file +} diff --git a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs index d2f7be1a..abed7c92 100644 --- a/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/MainWindow.xaml.cs @@ -1,9 +1,11 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using CommunityToolkit.Mvvm.Messaging; using Microsoft.UI.Xaml; using Snap.Hutao.Context.Database; using Snap.Hutao.Core.Windowing; +using Snap.Hutao.Message; namespace Snap.Hutao; @@ -15,6 +17,7 @@ public sealed partial class MainWindow : Window { private readonly AppDbContext appDbContext; private readonly WindowManager windowManager; + private readonly IMessenger messenger; private readonly TaskCompletionSource initializaionCompletionSource = new(); @@ -22,20 +25,22 @@ public sealed partial class MainWindow : Window /// 构造一个新的主窗体 /// /// 数据库上下文 - /// 日志器 - public MainWindow(AppDbContext appDbContext, ILogger logger) + /// 消息器 + public MainWindow(AppDbContext appDbContext, IMessenger messenger) { this.appDbContext = appDbContext; + this.messenger = messenger; + InitializeComponent(); windowManager = new WindowManager(this, TitleBarView.DragableArea); initializaionCompletionSource.TrySetResult(); } - - private void MainWindowClosed(object sender, WindowEventArgs args) { + messenger.Send(new MainWindowClosedMessage()); + windowManager?.Dispose(); // save userdata datebase diff --git a/src/Snap.Hutao/Snap.Hutao/Message/AchievementArchiveChangedMessage.cs b/src/Snap.Hutao/Snap.Hutao/Message/AchievementArchiveChangedMessage.cs new file mode 100644 index 00000000..88af10bb --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Message/AchievementArchiveChangedMessage.cs @@ -0,0 +1,22 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Entity; + +namespace Snap.Hutao.Message; + +/// +/// 成就存档切换消息 +/// +internal class AchievementArchiveChangedMessage : ValueChangedMessage +{ + /// + /// 构造一个新的用户切换消息 + /// + /// 老用户 + /// 新用户 + public AchievementArchiveChangedMessage(AchievementArchive? oldArchive, AchievementArchive? newArchive) + : base(oldArchive, newArchive) + { + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Message/MainWindowClosedMessage.cs b/src/Snap.Hutao/Snap.Hutao/Message/MainWindowClosedMessage.cs new file mode 100644 index 00000000..9b23aae7 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Message/MainWindowClosedMessage.cs @@ -0,0 +1,13 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding; + +namespace Snap.Hutao.Message; + +/// +/// 主窗体关闭消息 +/// +internal class MainWindowClosedMessage +{ +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Message/UserChangedMessage.cs b/src/Snap.Hutao/Snap.Hutao/Message/UserChangedMessage.cs new file mode 100644 index 00000000..19ffd27d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Message/UserChangedMessage.cs @@ -0,0 +1,22 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding; + +namespace Snap.Hutao.Message; + +/// +/// 用户切换消息 +/// +internal class UserChangedMessage : ValueChangedMessage +{ + /// + /// 构造一个新的用户切换消息 + /// + /// 老用户 + /// 新用户 + public UserChangedMessage(User? oldUser, User? newUser) + : base(oldUser, newUser) + { + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Message/ValueChangedMessage.cs b/src/Snap.Hutao/Snap.Hutao/Message/ValueChangedMessage.cs new file mode 100644 index 00000000..4af90ee3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Message/ValueChangedMessage.cs @@ -0,0 +1,33 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Message; + +/// +/// 值变化消息 +/// +/// 值的类型 +internal abstract class ValueChangedMessage + where TValue : class +{ + /// + /// 构造一个新的值变化消息 + /// + /// 旧值 + /// 新值 + public ValueChangedMessage(TValue? oldValue, TValue? newValue) + { + OldValue = oldValue; + NewValue = newValue; + } + + /// + /// 旧的值 + /// + public TValue? OldValue { get; private set; } + + /// + /// 新的值 + /// + public TValue? NewValue { get; private set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20220813040006_AddAchievement.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20220813040006_AddAchievement.Designer.cs new file mode 100644 index 00000000..b188da55 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20220813040006_AddAchievement.Designer.cs @@ -0,0 +1,90 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Snap.Hutao.Context.Database; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220813040006_AddAchievement")] + partial class AddAchievement + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.8"); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Current") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("UserId"); + + b.ToTable("achievements"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("settings"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Cookie") + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.HasOne("Snap.Hutao.Model.Entity.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20220813040006_AddAchievement.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20220813040006_AddAchievement.cs new file mode 100644 index 00000000..08df22c6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20220813040006_AddAchievement.cs @@ -0,0 +1,45 @@ +// +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + public partial class AddAchievement : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "achievements", + columns: table => new + { + InnerId = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + Id = table.Column(type: "INTEGER", nullable: false), + Current = table.Column(type: "INTEGER", nullable: false), + Time = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_achievements", x => x.InnerId); + table.ForeignKey( + name: "FK_achievements_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "InnerId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_achievements_UserId", + table: "achievements", + column: "UserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "achievements"); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20220815133601_AddAchievementArchive.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20220815133601_AddAchievementArchive.Designer.cs new file mode 100644 index 00000000..19ef034d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20220815133601_AddAchievementArchive.Designer.cs @@ -0,0 +1,111 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Snap.Hutao.Context.Database; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20220815133601_AddAchievementArchive")] + partial class AddAchievementArchive + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.8"); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("Current") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("achievements"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("achievement_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("settings"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Cookie") + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.HasKey("InnerId"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive") + .WithMany() + .HasForeignKey("ArchiveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20220815133601_AddAchievementArchive.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20220815133601_AddAchievementArchive.cs new file mode 100644 index 00000000..e744a3a6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20220815133601_AddAchievementArchive.cs @@ -0,0 +1,87 @@ +// +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + public partial class AddAchievementArchive : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_achievements_users_UserId", + table: "achievements"); + + migrationBuilder.RenameColumn( + name: "UserId", + table: "achievements", + newName: "ArchiveId"); + + migrationBuilder.RenameIndex( + name: "IX_achievements_UserId", + table: "achievements", + newName: "IX_achievements_ArchiveId"); + + migrationBuilder.AddColumn( + name: "Status", + table: "achievements", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "achievement_archives", + columns: table => new + { + InnerId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + IsSelected = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_achievement_archives", x => x.InnerId); + }); + + migrationBuilder.AddForeignKey( + name: "FK_achievements_achievement_archives_ArchiveId", + table: "achievements", + column: "ArchiveId", + principalTable: "achievement_archives", + principalColumn: "InnerId", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_achievements_achievement_archives_ArchiveId", + table: "achievements"); + + migrationBuilder.DropTable( + name: "achievement_archives"); + + migrationBuilder.DropColumn( + name: "Status", + table: "achievements"); + + migrationBuilder.RenameColumn( + name: "ArchiveId", + table: "achievements", + newName: "UserId"); + + migrationBuilder.RenameIndex( + name: "IX_achievements_ArchiveId", + table: "achievements", + newName: "IX_achievements_UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_achievements_users_UserId", + table: "achievements", + column: "UserId", + principalTable: "users", + principalColumn: "InnerId", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs index 5ebf4ec0..af836a9b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/AppDbContextModelSnapshot.cs @@ -1,6 +1,8 @@ // +using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Snap.Hutao.Context.Database; #nullable disable @@ -13,7 +15,53 @@ namespace Snap.Hutao.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.7"); + modelBuilder.HasAnnotation("ProductVersion", "6.0.8"); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ArchiveId") + .HasColumnType("TEXT"); + + b.Property("Current") + .HasColumnType("INTEGER"); + + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.HasIndex("ArchiveId"); + + b.ToTable("achievements"); + }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b => + { + b.Property("InnerId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("InnerId"); + + b.ToTable("achievement_archives"); + }); modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => { @@ -44,6 +92,17 @@ namespace Snap.Hutao.Migrations b.ToTable("users"); }); + + modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => + { + b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive") + .WithMany() + .HasForeignKey("ArchiveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Achievement.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Achievement.cs new file mode 100644 index 00000000..81510aa1 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Achievement.cs @@ -0,0 +1,63 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Binding; + +/// +/// 用于视图绑定的成就 +/// +public class Achievement : Observable +{ + /// + /// 满进度占位符 + /// + public const int FullProgressPlaceholder = int.MaxValue; + + private readonly Metadata.Achievement.Achievement inner; + private readonly Entity.Achievement entity; + + private bool isChecked; + + /// + /// 构造一个新的成就 + /// + /// 元数据部分 + /// 实体部分 + public Achievement(Metadata.Achievement.Achievement inner, Entity.Achievement entity) + { + this.inner = inner; + this.entity = entity; + + // Property should only be set when is user checking. + isChecked = (int)entity.Status >= 2; + } + + /// + /// 实体 + /// + public Entity.Achievement Entity { get => entity; } + + /// + /// 元数据 + /// + public Metadata.Achievement.Achievement Inner { get => inner; } + + /// + /// 是否选中 + /// + public bool IsChecked + { + get => isChecked; + set + { + Set(ref isChecked, value); + + // Only update state when checked + if (value) + { + Entity.Status = Intrinsic.AchievementInfoStatus.ACHIEVEMENT_POINT_TAKEN; + Entity.Time = DateTimeOffset.Now; + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Achievement.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Achievement.cs new file mode 100644 index 00000000..55b400d1 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Achievement.cs @@ -0,0 +1,118 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Extension; +using Snap.Hutao.Model.InterChange.Achievement; +using Snap.Hutao.Model.Intrinsic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Snap.Hutao.Model.Entity; + +/// +/// 成就 +/// +[Table("achievements")] +public class Achievement : IEquatable +{ + /// + /// 内部Id + /// + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid InnerId { get; set; } + + /// + /// 完成成就的用户 + /// + [ForeignKey(nameof(ArchiveId))] + public AchievementArchive Archive { get; set; } = default!; + + /// + /// 完成成就的用户Id + /// + public Guid ArchiveId { get; set; } + + /// + /// Id + /// + public int Id { get; set; } + + /// + /// 当前进度 + /// + public int Current { get; set; } + + /// + /// 完成时间 + /// + public DateTimeOffset Time { get; set; } + + /// + /// 状态 + /// + public AchievementInfoStatus Status { get; set; } + + /// + /// 创建一个新的成就 + /// + /// 对应的用户id + /// 成就Id + /// 新创建的成就 + public static Achievement Create(Guid userId, int id) + { + return new() + { + ArchiveId = userId, + Id = id, + Current = 0, + Time = DateTimeOffset.MinValue, + }; + } + + /// + /// 创建一个新的成就 + /// + /// 对应的用户id + /// uiaf项 + /// 新创建的成就 + public static Achievement Create(Guid userId, UIAFItem uiaf) + { + return new() + { + ArchiveId = userId, + Id = uiaf.Id, + Current = uiaf.Current, + Time = DateTimeOffset.FromUnixTimeSeconds(uiaf.Timestamp).ToLocalTime(true), + }; + } + + /// + public bool Equals(Achievement? other) + { + if (other == null) + { + return false; + } + else + { + return ArchiveId == other.ArchiveId + && Id == other.Id + && Current == other.Current + && Status == other.Status + && Time == other.Time; + } + } + + /// + public override bool Equals(object? obj) + { + return Equals(obj as Achievement); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(ArchiveId, Id, Current, Status, Time); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/AchievementArchive.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/AchievementArchive.cs new file mode 100644 index 00000000..de7dace3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/AchievementArchive.cs @@ -0,0 +1,41 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Snap.Hutao.Model.Entity; + +/// +/// 成就存档 +/// +[Table("achievement_archives")] +public class AchievementArchive +{ + /// + /// 内部Id + /// + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid InnerId { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } = default!; + + /// + /// 是否选中 + /// + public bool IsSelected { get; set; } + + /// + /// 创建一个新的存档 + /// + /// 名称 + /// 新存档 + public static AchievementArchive Create(string name) + { + return new() { Name = name }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs index e2420325..f1650eff 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs @@ -28,4 +28,14 @@ public class User /// 用户的Cookie /// public string? Cookie { get; set; } -} \ No newline at end of file + + /// + /// 创建一个新的用户 + /// + /// cookie + /// 新创建的用户 + public static User Create(string cookie) + { + return new() { Cookie = cookie }; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAF.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAF.cs new file mode 100644 index 00000000..a065f73c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAF.cs @@ -0,0 +1,24 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Snap.Hutao.Model.InterChange.Achievement; + +/// +/// 统一可交换成就格式 +/// +public class UIAF +{ + /// + /// 信息 + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public UIAFInfo Info { get; set; } = default!; + + /// + /// 列表 + /// + public List List { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs new file mode 100644 index 00000000..7cdb5ec6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFInfo.cs @@ -0,0 +1,44 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json.Serialization; + +namespace Snap.Hutao.Model.InterChange.Achievement; + +/// +/// UIAF格式的信息 +/// +public class UIAFInfo +{ + /// + /// 导出的 App 名称 + /// + [JsonPropertyName("export_app")] + public string ExportApp { get; set; } = default!; + + /// + /// 导出的时间戳 + /// + [JsonPropertyName("export_timestamp")] + public long? ExportTimestamp { get; set; } + + /// + /// 导出时间 + /// + public DateTimeOffset ExportDateTime + { + get => DateTimeOffset.FromUnixTimeSeconds(ExportTimestamp ?? 0); + } + + /// + /// 导出的 App 版本 + /// + [JsonPropertyName("export_app_version")] + public string? ExportAppVersion { get; set; } + + /// + /// 使用的UIAF版本 + /// + [JsonPropertyName("uiaf_version")] + public string? UIAFVersion { get; set; } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFItem.cs b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFItem.cs new file mode 100644 index 00000000..ec85a5ab --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/InterChange/Achievement/UIAFItem.cs @@ -0,0 +1,33 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Intrinsic; + +namespace Snap.Hutao.Model.InterChange.Achievement; + +/// +/// UIAF 项 +/// +public class UIAFItem +{ + /// + /// 成就Id + /// + public int Id { get; set; } + + /// + /// 完成时间 + /// + public long Timestamp { get; set; } + + /// + /// 当前值 + /// 对于progress为1的项,该属性始终为0 + /// + public int Current { get; set; } + + /// + /// 完成状态 + /// + public AchievementInfoStatus Status { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/AchievementInfoStatus.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/AchievementInfoStatus.cs new file mode 100644 index 00000000..9c953af3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/AchievementInfoStatus.cs @@ -0,0 +1,31 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Intrinsic; + +/// +/// 成就信息状态 +/// https://github.com/Grasscutters/Grasscutter/blob/development/proto/AchievementInfo.proto +/// +public enum AchievementInfoStatus +{ + /// + /// 非法值 + /// + ACHIEVEMENT_INVALID = 0, + + /// + /// 未完成 + /// + ACHIEVEMENT_UNFINISHED = 1, + + /// + /// 已完成 + /// + ACHIEVEMENT_FINISHED = 2, + + /// + /// 奖励已领取 + /// + ACHIEVEMENT_POINT_TAKEN = 3, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ElementType.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ElementType.cs index d6450384..2b64164f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ElementType.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ElementType.cs @@ -63,4 +63,4 @@ public enum ElementType /// 默认 /// Default = 255, -} \ No newline at end of file +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Observable.cs b/src/Snap.Hutao/Snap.Hutao/Model/Observable.cs index 0d75396d..9b0db94f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Observable.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Observable.cs @@ -8,7 +8,7 @@ namespace Snap.Hutao.Model; /// /// 简单的实现了 接口 /// -public class Observable : INotifyPropertyChanged +public abstract class Observable : INotifyPropertyChanged { /// public event PropertyChangedEventHandler? PropertyChanged; @@ -20,15 +20,17 @@ public class Observable : INotifyPropertyChanged /// 现有值 /// 新的值 /// 属性名称 - protected void Set([NotNullIfNotNull("value")] ref T storage, T value, [CallerMemberName] string propertyName = default!) + /// 项是否更新 + protected bool Set([NotNullIfNotNull("value")] ref T storage, T value, [CallerMemberName] string propertyName = default!) { if (Equals(storage, value)) { - return; + return false; } storage = value; OnPropertyChanged(propertyName); + return true; } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest index ce80236f..a62d8e70 100644 --- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest +++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest @@ -9,7 +9,7 @@ + Version="1.0.28.0" /> 胡桃 diff --git a/src/Snap.Hutao/Snap.Hutao/Program.cs b/src/Snap.Hutao/Snap.Hutao/Program.cs index f13b5d71..818c9fd0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Program.cs +++ b/src/Snap.Hutao/Snap.Hutao/Program.cs @@ -29,6 +29,7 @@ public static class Program [STAThread] private static void Main(string[] args) { + _ = args; XamlCheckProcessRequirements(); ComWrappersSupport.InitializeComWrappers(); diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ItemIcon_201.png b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ItemIcon_201.png new file mode 100644 index 0000000000000000000000000000000000000000..20eb573fbcaa8540d5ec01ae5ac61852b07991ee GIT binary patch literal 29654 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 QH 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 literal 0 HcmV?d00001 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs index 107df8aa..c8a924e7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs @@ -1,6 +1,19 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using CommunityToolkit.Mvvm.Messaging; +using Snap.Hutao.Context.Database; +using Snap.Hutao.Core.Diagnostics; +using Snap.Hutao.Core.Logging; +using Snap.Hutao.Model.InterChange.Achievement; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using BindingAchievement = Snap.Hutao.Model.Binding.Achievement; +using EntityAchievement = Snap.Hutao.Model.Entity.Achievement; +using EntityArchive = Snap.Hutao.Model.Entity.AchievementArchive; +using MetadataAchievement = Snap.Hutao.Model.Metadata.Achievement.Achievement; + namespace Snap.Hutao.Service.Achievement; /// @@ -9,5 +22,352 @@ namespace Snap.Hutao.Service.Achievement; [Injection(InjectAs.Transient, typeof(IAchievementService))] internal class AchievementService : IAchievementService { + private readonly AppDbContext appDbContext; + private readonly ILogger logger; + private readonly IMessenger messenger; -} + private EntityArchive? currentArchive; + private ObservableCollection? archiveCollection; + + /// + /// 构造一个新的成就服务 + /// + /// 数据库上下文 + /// 日志器 + /// 消息器 + public AchievementService(AppDbContext appDbContext, ILogger logger, IMessenger messenger) + { + this.appDbContext = appDbContext; + this.logger = logger; + this.messenger = messenger; + } + + /// + public EntityArchive? CurrentArchive + { + get => currentArchive; + set + { + if (currentArchive == value) + { + return; + } + + // only update when not processing a deletion + if (value != null) + { + if (currentArchive != null) + { + currentArchive.IsSelected = false; + appDbContext.AchievementArchives.Update(currentArchive); + appDbContext.SaveChanges(); + } + } + + Message.AchievementArchiveChangedMessage message = new(currentArchive, value); + + currentArchive = value; + + if (currentArchive != null) + { + currentArchive.IsSelected = true; + appDbContext.AchievementArchives.Update(currentArchive); + appDbContext.SaveChanges(); + } + + messenger.Send(message); + } + } + + /// + public ObservableCollection GetArchiveCollection() + { + return archiveCollection ??= new(appDbContext.AchievementArchives.ToList()); + } + + /// + public List GetAchievements(EntityArchive archive, IList metadata) + { + Guid archiveId = archive.InnerId; + List entities = appDbContext.Achievements + .Where(a => a.ArchiveId == archiveId) + + // Important! Prevent multiple sql command for SingleOrDefault below. + .ToList(); + + List results = new(); + foreach (MetadataAchievement meta in metadata) + { + EntityAchievement entity = entities.SingleOrDefault(e => e.Id == meta.Id) + ?? EntityAchievement.Create(archiveId, meta.Id); + + results.Add(new(meta, entity)); + } + + return results; + } + + /// + public ImportResult ImportFromUIAF(EntityArchive archive, List list, ImportOption option) + { + Guid archiveId = archive.InnerId; + + switch (option) + { + case ImportOption.AggressiveMerge: + { + IOrderedEnumerable orederedUIAF = list.OrderBy(a => a.Id); + return MergeAchievements(archiveId, orederedUIAF, true); + } + + case ImportOption.LazyMerge: + { + IOrderedEnumerable orederedUIAF = list.OrderBy(a => a.Id); + return MergeAchievements(archiveId, orederedUIAF, false); + } + + case ImportOption.Overwrite: + { + IEnumerable newData = list + .Select(uiaf => EntityAchievement.Create(archiveId, uiaf)) + .OrderBy(a => a.Id); + return OverwriteAchievements(archiveId, newData); + } + + default: + throw Must.NeverHappen(); + } + } + + /// + public Task RemoveArchiveAsync(EntityArchive archive) + { + Must.NotNull(archiveCollection!); + + // Sync cache + archiveCollection.Remove(archive); + + // Sync database + appDbContext.AchievementArchives.Remove(archive); + return appDbContext.SaveChangesAsync(); + } + + /// + public void SaveAchievements(EntityArchive archive, IList achievements) + { + string name = archive.Name; + logger.LogInformation(EventIds.Achievement, "Begin saving achievements for [{name}]", name); + ValueStopwatch stopwatch = ValueStopwatch.StartNew(); + + IEnumerable newData = achievements + .Where(a => a.IsChecked) + .Select(a => a.Entity) + .OrderBy(a => a.Id); + ImportResult result = OverwriteAchievements(archive.InnerId, newData); + + double time = stopwatch.GetElapsedTime().TotalMilliseconds; + logger.LogInformation(EventIds.Achievement, "{add} added, {update} updated, {remove} removed", result.Add, result.Update, result.Remove); + logger.LogInformation(EventIds.Achievement, "Save achievements for [{name}] completed in {time}ms", name, time); + } + + /// + public async Task TryAddArchiveAsync(EntityArchive newArchive) + { + if (string.IsNullOrEmpty(newArchive.Name)) + { + return ArchiveAddResult.InvalidName; + } + + Must.NotNull(archiveCollection!); + + // 查找是否有相同的名称 + if (archiveCollection.SingleOrDefault(a => a.Name == newArchive.Name) is EntityArchive userWithSameUid) + { + return ArchiveAddResult.AlreadyExists; + } + else + { + // Sync cache + archiveCollection.Add(newArchive); + + // Sync database + appDbContext.AchievementArchives.Add(newArchive); + await appDbContext.SaveChangesAsync().ConfigureAwait(false); + + return ArchiveAddResult.Added; + } + } + + private ImportResult MergeAchievements(Guid archiveId, IOrderedEnumerable orederedUIAF, bool aggressive) + { + IOrderedQueryable oldData = appDbContext.Achievements + .Where(a => a.ArchiveId == archiveId) + .OrderBy(a => a.Id); + + int add = 0; + int update = 0; + int remove = 0; + + using (IEnumerator entityEnumerator = oldData.GetEnumerator()) + { + using (IEnumerator uiafEnumerator = orederedUIAF.GetEnumerator()) + { + bool moveEntity = true; + bool moveUIAF = true; + + while (true) + { + bool moveEntityResult = moveEntity && entityEnumerator.MoveNext(); + bool moveUIAFResult = moveUIAF && uiafEnumerator.MoveNext(); + + if (!(moveEntityResult || moveUIAFResult)) + { + break; + } + else + { + EntityAchievement? entity = entityEnumerator.Current; + UIAFItem? uiaf = uiafEnumerator.Current; + + if (entity == null && uiaf != null) + { + AddEntity(EntityAchievement.Create(archiveId, uiaf)); + add++; + continue; + } + else if (entity != null && uiaf == null) + { + // skip + continue; + } + + if (entity!.Id < uiaf!.Id) + { + moveEntity = true; + moveUIAF = false; + } + else if (entity.Id == uiaf.Id) + { + moveEntity = true; + moveUIAF = true; + + if (aggressive) + { + RemoveEntity(entity); + AddEntity(EntityAchievement.Create(archiveId, uiaf)); + update++; + } + } + else + { + // entity.Id > uiaf.Id + moveEntity = false; + moveUIAF = true; + + AddEntity(EntityAchievement.Create(archiveId, uiaf)); + add++; + } + } + } + } + } + + return new(add, update, remove); + } + + private ImportResult OverwriteAchievements(Guid archiveId, IEnumerable newData) + { + IQueryable oldData = appDbContext.Achievements + .Where(a => a.ArchiveId == archiveId) + .OrderBy(a => a.Id); + + int add = 0; + int update = 0; + int remove = 0; + + using (IEnumerator oldDataEnumerator = oldData.GetEnumerator()) + { + using (IEnumerator newDataEnumerator = newData.GetEnumerator()) + { + bool moveOld = true; + bool moveNew = true; + + while (true) + { + bool moveOldResult = moveOld && oldDataEnumerator.MoveNext(); + bool moveNewResult = moveNew && newDataEnumerator.MoveNext(); + + if (!(moveOldResult || moveNewResult)) + { + break; + } + else + { + EntityAchievement? oldEntity = oldDataEnumerator.Current; + EntityAchievement? newEntity = newDataEnumerator.Current; + + if (oldEntity == null && newEntity != null) + { + AddEntity(newEntity); + add++; + continue; + } + else if (oldEntity != null && newEntity == null) + { + RemoveEntity(oldEntity); + remove++; + continue; + } + + if (oldEntity!.Id < newEntity!.Id) + { + moveOld = true; + moveNew = false; + RemoveEntity(oldEntity); + remove++; + } + else if (oldEntity.Id == newEntity.Id) + { + moveOld = true; + moveNew = true; + + if (oldEntity.Equals(newEntity)) + { + // skip same entry. + continue; + } + else + { + RemoveEntity(oldEntity); + AddEntity(newEntity); + update++; + } + } + else + { + // entity.Id > uiaf.Id + moveOld = false; + moveNew = true; + AddEntity(newEntity); + add++; + } + } + } + } + } + + return new(add, update, remove); + } + + private void AddEntity(EntityAchievement entity) + { + appDbContext.Achievements.Add(entity); + appDbContext.SaveChanges(); + } + + private void RemoveEntity(EntityAchievement entity) + { + appDbContext.Achievements.Remove(entity); + appDbContext.SaveChanges(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ArchiveAddResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ArchiveAddResult.cs new file mode 100644 index 00000000..82798eaa --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ArchiveAddResult.cs @@ -0,0 +1,25 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Achievement; + +/// +/// 存档添加结果 +/// +public enum ArchiveAddResult +{ + /// + /// 添加成功 + /// + Added, + + /// + /// 名称无效 + /// + InvalidName, + + /// + /// 已经存在该用户 + /// + AlreadyExists, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs index 1e242e21..40547697 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs @@ -1,9 +1,66 @@ -namespace Snap.Hutao.Service.Achievement; +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.InterChange.Achievement; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using BindingAchievement = Snap.Hutao.Model.Binding.Achievement; +using EntityArchive = Snap.Hutao.Model.Entity.AchievementArchive; +using MetadataAchievement = Snap.Hutao.Model.Metadata.Achievement.Achievement; + +namespace Snap.Hutao.Service.Achievement; /// /// 成就服务抽象 /// internal interface IAchievementService { + /// + /// 当前存档 + /// + EntityArchive? CurrentArchive { get; set; } + /// + /// 获取整合的成就 + /// + /// 用户 + /// 元数据 + /// 整合的成就 + List GetAchievements(EntityArchive archive, IList metadata); + + /// + /// 获取用于绑定的成就存档集合 + /// + /// 成就存档集合 + ObservableCollection GetArchiveCollection(); + + /// + /// 导入UIAF数据 + /// + /// 用户 + /// UIAF数据 + /// 选项 + /// 导入 + ImportResult ImportFromUIAF(EntityArchive archive, List list, ImportOption option); + + /// + /// 异步移除存档 + /// + /// 待移除的存档 + /// 任务 + Task RemoveArchiveAsync(EntityArchive archive); + + /// + /// 保存成就 + /// + /// 用户 + /// 成就 + void SaveAchievements(EntityArchive archive, IList achievements); + + /// + /// 尝试添加存档 + /// + /// 新存档 + /// 存档添加结果 + Task TryAddArchiveAsync(EntityArchive newArchive); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportOption.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportOption.cs new file mode 100644 index 00000000..8fe50d39 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportOption.cs @@ -0,0 +1,25 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Achievement; + +/// +/// 导入选项 +/// +public enum ImportOption +{ + /// + /// 贪婪合并 + /// + AggressiveMerge = 0, + + /// + /// 懒惰合并 + /// + LazyMerge = 1, + + /// + /// 完全覆盖 + /// + Overwrite = 2, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportResult.cs new file mode 100644 index 00000000..02b8a283 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/ImportResult.cs @@ -0,0 +1,38 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Achievement; + +/// +/// 导入结果 +/// +public struct ImportResult +{ + /// + /// 新增数 + /// + public readonly int Add; + + /// + /// 更新数 + /// + public readonly int Update; + + /// + /// 移除数 + /// + public readonly int Remove; + + /// + /// 构造一个新的导入结果 + /// + /// 添加数 + /// 更新数 + /// 移除数 + public ImportResult(int add, int update, int remove) + { + Add = add; + Update = update; + Remove = remove; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs b/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs index ad62f22e..e151d575 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/InfoBarService.cs @@ -12,7 +12,6 @@ internal class InfoBarService : IInfoBarService { private readonly TaskCompletionSource initializaionCompletionSource = new(); private StackPanel? infoBarStack; - /// public void Initialize(StackPanel container) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs index 87fc1297..7f9de76e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using CommunityToolkit.Mvvm.Messaging; using Snap.Hutao.Context.Database; using Snap.Hutao.Model.Binding; using Snap.Hutao.Service.Abstraction; @@ -22,6 +23,7 @@ internal class UserService : IUserService private readonly AppDbContext appDbContext; private readonly UserClient userClient; private readonly UserGameRoleClient userGameRoleClient; + private readonly IMessenger messenger; private User? currentUser; private ObservableCollection? userCollection = null; @@ -32,11 +34,13 @@ internal class UserService : IUserService /// 应用程序数据库上下文 /// 用户客户端 /// 角色客户端 - public UserService(AppDbContext appDbContext, UserClient userClient, UserGameRoleClient userGameRoleClient) + /// 消息器 + public UserService(AppDbContext appDbContext, UserClient userClient, UserGameRoleClient userGameRoleClient, IMessenger messenger) { this.appDbContext = appDbContext; this.userClient = userClient; this.userGameRoleClient = userGameRoleClient; + this.messenger = messenger; } /// @@ -45,6 +49,11 @@ internal class UserService : IUserService get => currentUser; set { + if (currentUser == value) + { + return; + } + // only update when not processing a deletion if (value != null) { @@ -56,6 +65,8 @@ internal class UserService : IUserService } } + Message.UserChangedMessage message = new(currentUser, value); + // 当删除到无用户时也能正常反应状态 currentUser = value; @@ -65,6 +76,8 @@ internal class UserService : IUserService appDbContext.Users.Update(currentUser.Entity); appDbContext.SaveChanges(); } + + messenger.Send(message); } } @@ -109,8 +122,10 @@ internal class UserService : IUserService /// public Task RemoveUserAsync(User user) { + Must.NotNull(userCollection!); + // Sync cache - userCollection!.Remove(user); + userCollection.Remove(user); // Sync database appDbContext.Users.Remove(user.Entity); @@ -152,7 +167,7 @@ internal class UserService : IUserService /// public Task CreateUserAsync(string cookie) { - return User.CreateAsync(new() { Cookie = cookie }, userClient, userGameRoleClient); + return User.CreateAsync(Model.Entity.User.Create(cookie), userClient, userGameRoleClient); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index f60bf593..7f19414f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -9,7 +9,9 @@ win10-x64 win10-$(Platform).pubxml true - false + False + False + False enable true zh-CN @@ -26,23 +28,20 @@ Snap.Hutao.Program $(DefineConstants);DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION;DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT - - embedded - - - embedded - + + + @@ -71,6 +70,7 @@ + @@ -78,20 +78,19 @@ - - - - + + + - - + all runtime; build; native; contentfiles; analyzers; buildtransitive + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -114,6 +113,16 @@ + + + MSBuild:Compile + + + + + MSBuild:Compile + + MSBuild:Compile diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml new file mode 100644 index 00000000..86378265 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml @@ -0,0 +1,22 @@ + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml.cs new file mode 100644 index 00000000..04998ea7 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementArchiveCreateDialog.xaml.cs @@ -0,0 +1,36 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Snap.Hutao.Core.Threading; + +namespace Snap.Hutao.View.Dialog; + +/// +/// 成就存档创建对话框 +/// +public sealed partial class AchievementArchiveCreateDialog : ContentDialog +{ + /// + /// 构造一个新的成就存档创建对话框 + /// + /// 窗体 + public AchievementArchiveCreateDialog(Window window) + { + InitializeComponent(); + XamlRoot = window.Content.XamlRoot; + } + + /// + /// 获取输入的字符串 + /// + /// 输入的结果 + public async Task> GetInputAsync() + { + ContentDialogResult result = await ShowAsync(); + string text = InputText.Text ?? string.Empty; + + return new(result == ContentDialogResult.Primary, text); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml new file mode 100644 index 00000000..f7d07f70 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml.cs new file mode 100644 index 00000000..f49a87d8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml.cs @@ -0,0 +1,52 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Snap.Hutao.Core; +using Snap.Hutao.Core.Threading; +using Snap.Hutao.Model.InterChange.Achievement; +using Snap.Hutao.Service.Achievement; + +namespace Snap.Hutao.View.Dialog; + +/// +/// 成就对话框 +/// +public sealed partial class AchievementImportDialog : ContentDialog +{ + private static readonly DependencyProperty UIAFProperty = Property.Depend(nameof(UIAF), default(UIAF)); + + /// + /// 构造一个新的成就对话框 + /// + /// 呈现的父窗口 + /// uiaf数据 + public AchievementImportDialog(Window window, UIAF uiaf) + { + InitializeComponent(); + XamlRoot = window.Content.XamlRoot; + UIAF = uiaf; + } + + /// + /// UIAF数据 + /// + public UIAF UIAF + { + get { return (UIAF)GetValue(UIAFProperty); } + set { SetValue(UIAFProperty, value); } + } + + /// + /// 异步获取导入选项 + /// + /// 导入选项 + public async Task> GetImportOptionAsync() + { + ContentDialogResult result = await ShowAsync(); + ImportOption option = (ImportOption)ImportModeSelector.SelectedIndex; + + return new Result(result == ContentDialogResult.Primary, option); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml index 8cba4f36..4a965821 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml @@ -22,13 +22,53 @@ + + + + + + + + + + + + + + + + + + + + + - - - + OpenPaneLength="252" + PaneBackground="Transparent"> - + @@ -86,28 +125,40 @@ MinHeight="48"> - - - - + + Text="{Binding Inner.Description}"/> + + + + + + + + @@ -115,6 +166,5 @@ - \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml.cs index 4a2dc3da..8e0c90d6 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using CommunityToolkit.Mvvm.Messaging; using Microsoft.UI.Xaml.Navigation; using Snap.Hutao.Control.Cancellable; using Snap.Hutao.Service.Navigation; @@ -32,4 +33,12 @@ public sealed partial class AchievementPage : CancellablePage extra.NotifyNavigationCompleted(); } } + + /// + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + ((AchievementViewModel)DataContext).SaveAchievements(); + + base.OnNavigatingFrom(e); + } } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementContentPage.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementContentPage.xaml.cs index e962abd0..cf3cecae 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementContentPage.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementContentPage.xaml.cs @@ -3,7 +3,6 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Navigation; -using Microsoft.VisualStudio.Threading; using Microsoft.Web.WebView2.Core; using Snap.Hutao.Extension; using Snap.Hutao.Service.Navigation; diff --git a/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml b/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml index 81fc5d1e..0eb5e869 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml @@ -1,9 +1,8 @@  [Injection(InjectAs.Transient)] -internal class AchievementViewModel : ObservableObject, ISupportCancellation +internal class AchievementViewModel + : ObservableObject, + ISupportCancellation, + IRecipient, + IRecipient { private readonly IMetadataService metadataService; + private readonly IAchievementService achievementService; + private readonly IInfoBarService infoBarService; + private readonly JsonSerializerOptions options; + private readonly IPickerFactory pickerFactory; + private AdvancedCollectionView? achievements; private IList? achievementGoals; private AchievementGoal? selectedAchievementGoal; + private ObservableCollection? archives; + private Model.Entity.AchievementArchive? selectedArchive; /// /// 构造一个新的成就视图模型 /// /// 元数据服务 + /// 成就服务 + /// 信息条服务 + /// Json序列化选项 /// 异步命令工厂 - public AchievementViewModel(IMetadataService metadataService, IAsyncRelayCommandFactory asyncRelayCommandFactory) + /// 文件选择器工厂 + /// 消息器 + public AchievementViewModel( + IMetadataService metadataService, + IAchievementService achievementService, + IInfoBarService infoBarService, + JsonSerializerOptions options, + IAsyncRelayCommandFactory asyncRelayCommandFactory, + IPickerFactory pickerFactory, + IMessenger messenger) { this.metadataService = metadataService; + this.achievementService = achievementService; + this.infoBarService = infoBarService; + this.options = options; + this.pickerFactory = pickerFactory; + OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync); + ImportUIAFFromClipboardCommand = asyncRelayCommandFactory.Create(ImportUIAFFromClipboardAsync); + ImportUIAFFromFileCommand = asyncRelayCommandFactory.Create(ImportUIAFFromFileAsync); + AddArchiveCommand = asyncRelayCommandFactory.Create(AddArchiveAsync); + RemoveArchiveCommand = asyncRelayCommandFactory.Create(RemoveArchiveAsync); + + messenger.Register(this); + messenger.Register(this); } /// public CancellationToken CancellationToken { get; set; } + /// + /// 成就存档集合 + /// + public ObservableCollection? Archives + { + get => archives; + set => SetProperty(ref archives, value); + } + + /// + /// 选中的成就存档 + /// + public Model.Entity.AchievementArchive? SelectedArchive + { + get => selectedArchive; + set + { + if (SetProperty(ref selectedArchive, value)) + { + achievementService.CurrentArchive = value; + } + } + } + /// /// 成就视图 /// @@ -63,7 +138,7 @@ internal class AchievementViewModel : ObservableObject, ISupportCancellation set { SetProperty(ref selectedAchievementGoal, value); - OnGoalChanged(value); + UpdateAchievementFilter(value); } } @@ -72,21 +147,249 @@ internal class AchievementViewModel : ObservableObject, ISupportCancellation /// public ICommand OpenUICommand { get; } + /// + /// 添加存档命令 + /// + public ICommand AddArchiveCommand { get; } + + /// + /// 删除存档命令 + /// + public ICommand RemoveArchiveCommand { get; } + + /// + /// 从剪贴板导入UIAF命令 + /// + public ICommand ImportUIAFFromClipboardCommand { get; } + + /// + /// 从文件导入UIAF命令 + /// + public ICommand ImportUIAFFromFileCommand { get; } + + /// + public void Receive(MainWindowClosedMessage message) + { + SaveAchievements(); + } + + /// + public void Receive(AchievementArchiveChangedMessage message) + { + HandleArchiveChangeAsync(message.OldValue, message.NewValue).SafeForget(); + } + + /// + /// 保存当前用户的成就 + /// + public void SaveAchievements() + { + if (Achievements != null && SelectedArchive != null) + { + achievementService.SaveAchievements(SelectedArchive, (Achievements.Source as IList)!); + } + } + + private async Task HandleArchiveChangeAsync(Model.Entity.AchievementArchive? oldArchieve, Model.Entity.AchievementArchive? newArchieve) + { + if (oldArchieve != null && Achievements != null) + { + achievementService.SaveAchievements(oldArchieve, (Achievements.Source as IList)!); + } + + if (newArchieve != null) + { + await UpdateAchievementsAsync(newArchieve); + } + else + { + infoBarService.Warning("请创建或选择一个成就存档"); + } + } + private async Task OpenUIAsync() { if (await metadataService.InitializeAsync(CancellationToken)) { - Achievements = new(await metadataService.GetAchievementsAsync(CancellationToken), true); AchievementGoals = await metadataService.GetAchievementGoalsAsync(CancellationToken); + + Archives = achievementService.GetArchiveCollection(); + + SelectedArchive = Archives.SingleOrDefault(a => a.IsSelected == true); + + if (SelectedArchive == null) + { + infoBarService.Warning("请创建或选择一个成就存档"); + } } } - private void OnGoalChanged(AchievementGoal? goal) + private async Task UpdateAchievementsAsync(Model.Entity.AchievementArchive archive) + { + List rawAchievements = await metadataService.GetAchievementsAsync(CancellationToken); + List combined = achievementService.GetAchievements(archive, rawAchievements); + Achievements = new(combined, true); + + UpdateAchievementFilter(SelectedAchievementGoal); + } + + private async Task AddArchiveAsync() + { + (bool isOk, string name) = await new AchievementArchiveCreateDialog(App.Window!).GetInputAsync(); + + if (isOk) + { + ArchiveAddResult result = await achievementService.TryAddArchiveAsync(Model.Entity.AchievementArchive.Create(name)); + + switch (result) + { + case ArchiveAddResult.Added: + infoBarService.Success($"存档 [{name}] 添加成功"); + break; + case ArchiveAddResult.InvalidName: + infoBarService.Information($"不能添加名称无效的存档"); + break; + case ArchiveAddResult.AlreadyExists: + infoBarService.Information($"不能添加名称重复的存档 [{name}]"); + break; + default: + throw Must.NeverHappen(); + } + } + } + + private async Task RemoveArchiveAsync() + { + if (Archives != null && SelectedArchive != null) + { + ContentDialogResult result = await new ContentDialog2(App.Window!) + { + Title = $"确定要删除存档 {SelectedArchive.Name} 吗?", + Content = "该操作是不可逆的,该存档和其内的所有成就状态会丢失。", + PrimaryButtonText = "确认", + CloseButtonText = "取消", + DefaultButton = ContentDialogButton.Close, + } + .ShowAsync(); + + if (result == ContentDialogResult.Primary) + { + await achievementService.RemoveArchiveAsync(SelectedArchive); + + // reselect first archive + SelectedArchive = Archives.FirstOrDefault(); + } + } + } + + private async Task ImportUIAFFromClipboardAsync() + { + if (achievementService.CurrentArchive == null) + { + // TODO: automatically create a archive. + infoBarService.Information("必须选择一个用户才能导入成就"); + return; + } + + DataPackageView view = Clipboard.GetContent(); + string json = await view.GetTextAsync(); + + UIAF? uiaf = null; + try + { + uiaf = JsonSerializer.Deserialize(json, options); + } + catch (Exception ex) + { + infoBarService?.Error(ex); + } + + if (uiaf != null) + { + (bool isOk, ImportOption option) = await new AchievementImportDialog(App.Window!, uiaf).GetImportOptionAsync(); + + if (isOk) + { + ImportResult result = achievementService.ImportFromUIAF(achievementService.CurrentArchive, uiaf.List, option); + infoBarService!.Success($"新增:{result.Add} 个成就 | 更新:{result.Update} 个成就 | 删除{result.Remove} 个成就"); + + await UpdateAchievementsAsync(achievementService.CurrentArchive); + } + } + else + { + await new ContentDialog2(App.Window!) + { + Title = "导入失败", + Content = "剪贴板中的内容格式不正确", + PrimaryButtonText = "确认", + DefaultButton = ContentDialogButton.Primary, + } + .ShowAsync(); + } + } + + private async Task ImportUIAFFromFileAsync() + { + if (achievementService.CurrentArchive == null) + { + infoBarService.Information("必须选择一个用户才能导入成就"); + return; + } + + FileOpenPicker picker = pickerFactory.GetFileOpenPicker(); + picker.SuggestedStartLocation = PickerLocationId.Desktop; + picker.FileTypeFilter.Add(".json"); + + if (await picker.PickSingleFileAsync() is StorageFile file) + { + UIAF? uiaf = null; + try + { + using (IRandomAccessStreamWithContentType fileSream = await file.OpenReadAsync()) + { + using (Stream stream = fileSream.AsStream()) + { + uiaf = await JsonSerializer.DeserializeAsync(stream, options); + } + } + } + catch (Exception ex) + { + infoBarService?.Error(ex); + } + + if (uiaf != null) + { + (bool isOk, ImportOption option) = await new AchievementImportDialog(App.Window!, uiaf).GetImportOptionAsync(); + + if (isOk) + { + ImportResult result = achievementService.ImportFromUIAF(achievementService.CurrentArchive, uiaf.List, option); + infoBarService!.Success($"新增:{result.Add} 个成就 | 更新:{result.Update} 个成就 | 删除{result.Remove} 个成就"); + await UpdateAchievementsAsync(achievementService.CurrentArchive); + } + } + else + { + await new ContentDialog2(App.Window!) + { + Title = "导入失败", + Content = "文件中的内容格式不正确", + PrimaryButtonText = "确认", + DefaultButton = ContentDialogButton.Primary, + } + .ShowAsync(); + } + } + } + + private void UpdateAchievementFilter(AchievementGoal? goal) { if (Achievements != null) { Achievements.Filter = goal != null - ? ((object o) => o is Achievement achi && achi.Goal == goal.Id) + ? ((object o) => o is Model.Binding.Achievement achi && achi.Inner.Goal == goal.Id) : ((object o) => true); } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs index 6fb7fa0d..80c183f4 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs @@ -123,13 +123,12 @@ internal class UserViewModel : ObservableObject private async Task AddUserAsync() { // Get cookie from user input - Window mainWindow = Ioc.Default.GetRequiredService(); - Result result = await new UserDialog(mainWindow).GetInputCookieAsync(); + (bool isOk, string cookie) = await new UserDialog(App.Window!).GetInputCookieAsync(); // User confirms the input - if (result.IsOk) + if (isOk) { - if (TryValidateCookie(userService.ParseCookie(result.Value), out IDictionary? filteredCookie)) + if (TryValidateCookie(userService.ParseCookie(cookie), out IDictionary? filteredCookie)) { string simplifiedCookie = string.Join(';', filteredCookie.Select(kvp => $"{kvp.Key}={kvp.Value}")); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/HttpClientDynamicSecretExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/HttpClientDynamicSecretExtensions.cs index 41f16e28..69deb591 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/HttpClientDynamicSecretExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/HttpClientDynamicSecretExtensions.cs @@ -28,7 +28,6 @@ internal static class HttpClientDynamicSecretExtensions /// 请求器 /// 选项 /// 地址 - /// post数据 /// 响应 public static IDynamicSecret2HttpClient UsingDynamicSecret2(this HttpClient httpClient, JsonSerializerOptions options, string url) { @@ -47,7 +46,6 @@ internal static class HttpClientDynamicSecretExtensions public static IDynamicSecret2HttpClient UsingDynamicSecret2(this HttpClient httpClient, JsonSerializerOptions options, string url, TValue data) where TValue : class { - httpClient.DefaultRequestHeaders.Set("DS", DynamicSecretProvider2.Create(options, url, data)); return new DynamicSecret2HttpClient(httpClient, options, url, data); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/Reward.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/Reward.cs index 416a7365..e40a478a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/Reward.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/Reward.cs @@ -6,6 +6,9 @@ using System.Text.Json.Serialization; namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward; +/// +/// 奖励 +/// public class Reward { /// diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInResult.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInResult.cs index 1d473885..6053c183 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/BbsSignReward/SignInResult.cs @@ -7,35 +7,36 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward; /// /// 签到结果 +/// https://docs.geetest.com/sensebot/apirefer/api/server /// public class SignInResult { /// - /// 通常是 "" + /// ??? /// [JsonPropertyName("code")] public string Code { get; set; } = default!; /// - /// 通常是 "" + /// 风控码 375 /// [JsonPropertyName("risk_code")] public int RiskCode { get; set; } /// - /// 通常是 "" + /// geetest appid /// [JsonPropertyName("gt")] public string Gt { get; set; } = default!; /// - /// 通常是 "" + /// geetest challenge id /// [JsonPropertyName("challenge")] public string Challenge { get; set; } = default!; /// - /// 通常是 "" + /// geetest 服务状态 /// [JsonPropertyName("success")] public int Success { get; set; }