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