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 00000000..20eb573f Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_ItemIcon_201.png differ 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; }