Achievement functional

This commit is contained in:
DismissedLight
2022-08-16 14:29:33 +08:00
parent bd23e081fc
commit 641a731a4d
54 changed files with 2169 additions and 93 deletions

View File

@@ -11,7 +11,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.3" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -4,8 +4,8 @@
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.VisualStudio.Threading;
using Microsoft.Windows.AppLifecycle; using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Logging; using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension; using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Abstraction;
@@ -23,6 +23,7 @@ public partial class App : Application
{ {
private static Window? window; private static Window? window;
private readonly ILogger<App> logger; private readonly ILogger<App> logger;
private readonly ExceptionRecorder exceptionRecorder;
/// <summary> /// <summary>
/// Initializes the singleton application object. /// Initializes the singleton application object.
@@ -37,9 +38,7 @@ public partial class App : Application
// so we can use Ioc here. // so we can use Ioc here.
logger = Ioc.Default.GetRequiredService<ILogger<App>>(); logger = Ioc.Default.GetRequiredService<ILogger<App>>();
UnhandledException += AppUnhandledException; exceptionRecorder = new(this, logger);
DebugSettings.BindingFailed += XamlBindingFailed;
TaskScheduler.UnobservedTaskException += TaskSchedulerUnobservedTaskException;
} }
/// <summary> /// <summary>
@@ -74,13 +73,11 @@ public partial class App : Application
if (firstInstance.IsCurrent) if (firstInstance.IsCurrent)
{ {
OnActivated(firstInstance, activatedEventArgs);
firstInstance.Activated += OnActivated; firstInstance.Activated += OnActivated;
Window = Ioc.Default.GetRequiredService<MainWindow>();
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", CacheFolder.Path); logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", CacheFolder.Path);
OnActivated(firstInstance, activatedEventArgs);
Ioc.Default Ioc.Default
.GetRequiredService<IMetadataService>() .GetRequiredService<IMetadataService>()
.ImplictAs<IMetadataInitializer>()? .ImplictAs<IMetadataInitializer>()?
@@ -106,10 +103,10 @@ public partial class App : Application
.AddMemoryCache() .AddMemoryCache()
// Hutao extensions // Hutao extensions
.AddJsonSerializerOptions()
.AddDatebase()
.AddInjections() .AddInjections()
.AddHttpClients() .AddHttpClients()
.AddDatebase()
.AddJsonSerializerOptions()
// Discrete services // Discrete services
.AddSingleton<IMessenger>(WeakReferenceMessenger.Default) .AddSingleton<IMessenger>(WeakReferenceMessenger.Default)
@@ -123,9 +120,10 @@ public partial class App : Application
[SuppressMessage("", "VSTHRD100")] [SuppressMessage("", "VSTHRD100")]
private async void OnActivated(object? sender, AppActivationArguments args) private async void OnActivated(object? sender, AppActivationArguments args)
{ {
Window = Ioc.Default.GetRequiredService<MainWindow>();
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>(); IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
await infoBarService.WaitInitializationAsync(); await infoBarService.WaitInitializationAsync();
infoBarService.Information("OnActivated");
if (args.Kind == ExtendedActivationKind.Protocol) 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);
}
} }

View File

@@ -21,15 +21,25 @@ public class AppDbContext : DbContext
} }
/// <summary> /// <summary>
/// 设置 /// 设置
/// </summary> /// </summary>
public DbSet<SettingEntry> Settings { get; set; } = default!; public DbSet<SettingEntry> Settings { get; set; } = default!;
/// <summary> /// <summary>
/// 用户 /// 用户
/// </summary> /// </summary>
public DbSet<User> Users { get; set; } = default!; public DbSet<User> Users { get; set; } = default!;
/// <summary>
/// 成就
/// </summary>
public DbSet<Achievement> Achievements { get; set; } = default!;
/// <summary>
/// 成就存档
/// </summary>
public DbSet<AchievementArchive> AchievementArchives { get; set; } = default!;
/// <summary> /// <summary>
/// 构造一个临时的应用程序数据库上下文 /// 构造一个临时的应用程序数据库上下文
/// </summary> /// </summary>

View File

@@ -44,4 +44,4 @@ internal class InvokeCommandOnLoadedBehavior : BehaviorBase<UIElement>
base.OnAssociatedObjectLoaded(); base.OnAssociatedObjectLoaded();
} }
} }

View File

@@ -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;
/// <summary>
/// 在元素卸载完成后执行命令的行为
/// </summary>
internal class InvokeCommandOnUnloadedBehavior : BehaviorBase<UIElement>
{
private static readonly DependencyProperty CommandProperty = Property<InvokeCommandOnUnloadedBehavior>.Depend<ICommand>(nameof(Command));
/// <summary>
/// 待执行的命令
/// </summary>
public ICommand Command
{
get => (ICommand)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
/// <inheritdoc/>
protected override void OnDetaching()
{
if (Command != null && Command.CanExecute(null))
{
Command.Execute(null);
}
base.OnDetaching();
}
}

View File

@@ -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;
/// <summary>
/// 继承自 <see cref="ContentDialog"/> 实现了某些便捷功能
/// </summary>
internal class ContentDialog2 : ContentDialog
{
/// <summary>
/// 构造一个新的对话框
/// </summary>
/// <param name="window">窗口</param>
public ContentDialog2(Window window)
{
DefaultStyleKey = typeof(ContentDialog);
XamlRoot = window.Content.XamlRoot;
Interaction.SetBehaviors(this, new() { new ContentDialogBehavior() });
}
}

View File

@@ -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;
/// <summary>
/// 异常记录器
/// </summary>
internal class ExceptionRecorder
{
private readonly ILogger logger;
/// <summary>
/// 构造一个新的异常记录器
/// </summary>
/// <param name="application">应用程序</param>
/// <param name="logger">日志器</param>
public ExceptionRecorder(Application application, ILogger logger)
{
this.logger = logger;
application.UnhandledException += OnAppUnhandledException;
application.DebugSettings.BindingFailed += OnXamlBindingFailed;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
}
/// <summary>
/// 当应用程序未经处理的异常引发时调用
/// </summary>
/// <param name="sender">实例</param>
/// <param name="e">事件参数</param>
public void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常: [HResult:{code}]", e.Exception.HResult);
}
/// <summary>
/// Xaml 绑定失败时触发
/// </summary>
/// <param name="sender">实例</param>
/// <param name="e">事件参数</param>
public void OnXamlBindingFailed(object? sender, BindingFailedEventArgs e)
{
logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message);
}
/// <summary>
/// 异步命令异常时触发
/// </summary>
/// <param name="sender">实例</param>
/// <param name="e">事件参数</param>
public void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
logger.LogCritical(EventIds.UnobservedTaskException, "异步任务执行异常: {message}", e.Exception);
}
}

View File

@@ -73,6 +73,11 @@ internal static class EventIds
/// </summary> /// </summary>
public static readonly EventId FileCaching = 100120; public static readonly EventId FileCaching = 100120;
/// <summary>
/// 文件缓存
/// </summary>
public static readonly EventId Achievement = 100130;
// 杂项 // 杂项
/// <summary> /// <summary>

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="DateTimeOffset"/> 扩展
/// </summary>
public static class DateTimeOffsetExtensions
{
/// <summary>
/// Converts the current <see cref="DateTimeOffset"/> to a <see cref="DateTimeOffset"/> that represents the local time.
/// </summary>
/// <param name="dateTimeOffset">时间偏移</param>
/// <param name="keepTicks">保留主时间部分</param>
/// <returns>A <see cref="DateTimeOffset"/> that represents the local time.</returns>
public static DateTimeOffset ToLocalTime(this DateTimeOffset dateTimeOffset, bool keepTicks)
{
if (keepTicks)
{
dateTimeOffset += TimeZoneInfo.Local.GetUtcOffset(DateTimeOffset.Now).Negate();
}
return dateTimeOffset.ToLocalTime();
}
}

View File

@@ -22,4 +22,4 @@ public static class TupleExtensions
{ {
return new Dictionary<TKey, TValue>(1) { { tuple.Key, tuple.Value } }; return new Dictionary<TKey, TValue>(1) { { tuple.Key, tuple.Value } };
} }
} }

View File

@@ -1,9 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Snap.Hutao.Context.Database; using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Windowing; using Snap.Hutao.Core.Windowing;
using Snap.Hutao.Message;
namespace Snap.Hutao; namespace Snap.Hutao;
@@ -15,6 +17,7 @@ public sealed partial class MainWindow : Window
{ {
private readonly AppDbContext appDbContext; private readonly AppDbContext appDbContext;
private readonly WindowManager windowManager; private readonly WindowManager windowManager;
private readonly IMessenger messenger;
private readonly TaskCompletionSource initializaionCompletionSource = new(); private readonly TaskCompletionSource initializaionCompletionSource = new();
@@ -22,20 +25,22 @@ public sealed partial class MainWindow : Window
/// 构造一个新的主窗体 /// 构造一个新的主窗体
/// </summary> /// </summary>
/// <param name="appDbContext">数据库上下文</param> /// <param name="appDbContext">数据库上下文</param>
/// <param name="logger">日志器</param> /// <param name="messenger">消息器</param>
public MainWindow(AppDbContext appDbContext, ILogger<MainWindow> logger) public MainWindow(AppDbContext appDbContext, IMessenger messenger)
{ {
this.appDbContext = appDbContext; this.appDbContext = appDbContext;
this.messenger = messenger;
InitializeComponent(); InitializeComponent();
windowManager = new WindowManager(this, TitleBarView.DragableArea); windowManager = new WindowManager(this, TitleBarView.DragableArea);
initializaionCompletionSource.TrySetResult(); initializaionCompletionSource.TrySetResult();
} }
private void MainWindowClosed(object sender, WindowEventArgs args) private void MainWindowClosed(object sender, WindowEventArgs args)
{ {
messenger.Send(new MainWindowClosedMessage());
windowManager?.Dispose(); windowManager?.Dispose();
// save userdata datebase // save userdata datebase

View File

@@ -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;
/// <summary>
/// 成就存档切换消息
/// </summary>
internal class AchievementArchiveChangedMessage : ValueChangedMessage<AchievementArchive>
{
/// <summary>
/// 构造一个新的用户切换消息
/// </summary>
/// <param name="oldArchive">老用户</param>
/// <param name="newArchive">新用户</param>
public AchievementArchiveChangedMessage(AchievementArchive? oldArchive, AchievementArchive? newArchive)
: base(oldArchive, newArchive)
{
}
}

View File

@@ -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;
/// <summary>
/// 主窗体关闭消息
/// </summary>
internal class MainWindowClosedMessage
{
}

View File

@@ -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;
/// <summary>
/// 用户切换消息
/// </summary>
internal class UserChangedMessage : ValueChangedMessage<User>
{
/// <summary>
/// 构造一个新的用户切换消息
/// </summary>
/// <param name="oldUser">老用户</param>
/// <param name="newUser">新用户</param>
public UserChangedMessage(User? oldUser, User? newUser)
: base(oldUser, newUser)
{
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Message;
/// <summary>
/// 值变化消息
/// </summary>
/// <typeparam name="TValue">值的类型</typeparam>
internal abstract class ValueChangedMessage<TValue>
where TValue : class
{
/// <summary>
/// 构造一个新的值变化消息
/// </summary>
/// <param name="oldValue">旧值</param>
/// <param name="newValue">新值</param>
public ValueChangedMessage(TValue? oldValue, TValue? newValue)
{
OldValue = oldValue;
NewValue = newValue;
}
/// <summary>
/// 旧的值
/// </summary>
public TValue? OldValue { get; private set; }
/// <summary>
/// 新的值
/// </summary>
public TValue? NewValue { get; private set; }
}

View File

@@ -0,0 +1,90 @@
// <auto-generated />
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<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<int>("Current")
.HasColumnType("INTEGER");
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("UserId");
b.ToTable("achievements");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("settings");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Cookie")
.HasColumnType("TEXT");
b.Property<bool>("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
}
}
}

View File

@@ -0,0 +1,45 @@
// <auto-generated />
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<Guid>(type: "TEXT", nullable: false),
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
Id = table.Column<int>(type: "INTEGER", nullable: false),
Current = table.Column<int>(type: "INTEGER", nullable: false),
Time = table.Column<DateTimeOffset>(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");
}
}
}

View File

@@ -0,0 +1,111 @@
// <auto-generated />
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<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("Current")
.HasColumnType("INTEGER");
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("achievements");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("achievement_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("settings");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Cookie")
.HasColumnType("TEXT");
b.Property<bool>("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
}
}
}

View File

@@ -0,0 +1,87 @@
// <auto-generated />
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<int>(
name: "Status",
table: "achievements",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "achievement_archives",
columns: table => new
{
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
IsSelected = table.Column<bool>(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);
}
}
}

View File

@@ -1,6 +1,8 @@
// <auto-generated /> // <auto-generated />
using System;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Context.Database; using Snap.Hutao.Context.Database;
#nullable disable #nullable disable
@@ -13,7 +15,53 @@ namespace Snap.Hutao.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #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<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("Current")
.HasColumnType("INTEGER");
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("achievements");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("achievement_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{ {
@@ -44,6 +92,17 @@ namespace Snap.Hutao.Migrations
b.ToTable("users"); 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 #pragma warning restore 612, 618
} }
} }

View File

@@ -0,0 +1,63 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding;
/// <summary>
/// 用于视图绑定的成就
/// </summary>
public class Achievement : Observable
{
/// <summary>
/// 满进度占位符
/// </summary>
public const int FullProgressPlaceholder = int.MaxValue;
private readonly Metadata.Achievement.Achievement inner;
private readonly Entity.Achievement entity;
private bool isChecked;
/// <summary>
/// 构造一个新的成就
/// </summary>
/// <param name="inner">元数据部分</param>
/// <param name="entity">实体部分</param>
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;
}
/// <summary>
/// 实体
/// </summary>
public Entity.Achievement Entity { get => entity; }
/// <summary>
/// 元数据
/// </summary>
public Metadata.Achievement.Achievement Inner { get => inner; }
/// <summary>
/// 是否选中
/// </summary>
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;
}
}
}
}

View File

@@ -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;
/// <summary>
/// 成就
/// </summary>
[Table("achievements")]
public class Achievement : IEquatable<Achievement>
{
/// <summary>
/// 内部Id
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; }
/// <summary>
/// 完成成就的用户
/// </summary>
[ForeignKey(nameof(ArchiveId))]
public AchievementArchive Archive { get; set; } = default!;
/// <summary>
/// 完成成就的用户Id
/// </summary>
public Guid ArchiveId { get; set; }
/// <summary>
/// Id
/// </summary>
public int Id { get; set; }
/// <summary>
/// 当前进度
/// </summary>
public int Current { get; set; }
/// <summary>
/// 完成时间
/// </summary>
public DateTimeOffset Time { get; set; }
/// <summary>
/// 状态
/// </summary>
public AchievementInfoStatus Status { get; set; }
/// <summary>
/// 创建一个新的成就
/// </summary>
/// <param name="userId">对应的用户id</param>
/// <param name="id">成就Id</param>
/// <returns>新创建的成就</returns>
public static Achievement Create(Guid userId, int id)
{
return new()
{
ArchiveId = userId,
Id = id,
Current = 0,
Time = DateTimeOffset.MinValue,
};
}
/// <summary>
/// 创建一个新的成就
/// </summary>
/// <param name="userId">对应的用户id</param>
/// <param name="uiaf">uiaf项</param>
/// <returns>新创建的成就</returns>
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),
};
}
/// <inheritdoc/>
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;
}
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return Equals(obj as Achievement);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(ArchiveId, Id, Current, Status, Time);
}
}

View File

@@ -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;
/// <summary>
/// 成就存档
/// </summary>
[Table("achievement_archives")]
public class AchievementArchive
{
/// <summary>
/// 内部Id
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 是否选中
/// </summary>
public bool IsSelected { get; set; }
/// <summary>
/// 创建一个新的存档
/// </summary>
/// <param name="name">名称</param>
/// <returns>新存档</returns>
public static AchievementArchive Create(string name)
{
return new() { Name = name };
}
}

View File

@@ -28,4 +28,14 @@ public class User
/// 用户的Cookie /// 用户的Cookie
/// </summary> /// </summary>
public string? Cookie { get; set; } public string? Cookie { get; set; }
}
/// <summary>
/// 创建一个新的用户
/// </summary>
/// <param name="cookie">cookie</param>
/// <returns>新创建的用户</returns>
public static User Create(string cookie)
{
return new() { Cookie = cookie };
}
}

View File

@@ -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;
/// <summary>
/// 统一可交换成就格式
/// </summary>
public class UIAF
{
/// <summary>
/// 信息
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public UIAFInfo Info { get; set; } = default!;
/// <summary>
/// 列表
/// </summary>
public List<UIAFItem> List { get; set; } = default!;
}

View File

@@ -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;
/// <summary>
/// UIAF格式的信息
/// </summary>
public class UIAFInfo
{
/// <summary>
/// 导出的 App 名称
/// </summary>
[JsonPropertyName("export_app")]
public string ExportApp { get; set; } = default!;
/// <summary>
/// 导出的时间戳
/// </summary>
[JsonPropertyName("export_timestamp")]
public long? ExportTimestamp { get; set; }
/// <summary>
/// 导出时间
/// </summary>
public DateTimeOffset ExportDateTime
{
get => DateTimeOffset.FromUnixTimeSeconds(ExportTimestamp ?? 0);
}
/// <summary>
/// 导出的 App 版本
/// </summary>
[JsonPropertyName("export_app_version")]
public string? ExportAppVersion { get; set; }
/// <summary>
/// 使用的UIAF版本
/// </summary>
[JsonPropertyName("uiaf_version")]
public string? UIAFVersion { get; set; }
}

View File

@@ -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;
/// <summary>
/// UIAF 项
/// </summary>
public class UIAFItem
{
/// <summary>
/// 成就Id
/// </summary>
public int Id { get; set; }
/// <summary>
/// 完成时间
/// </summary>
public long Timestamp { get; set; }
/// <summary>
/// 当前值
/// 对于progress为1的项该属性始终为0
/// </summary>
public int Current { get; set; }
/// <summary>
/// 完成状态
/// </summary>
public AchievementInfoStatus Status { get; set; }
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Intrinsic;
/// <summary>
/// 成就信息状态
/// https://github.com/Grasscutters/Grasscutter/blob/development/proto/AchievementInfo.proto
/// </summary>
public enum AchievementInfoStatus
{
/// <summary>
/// 非法值
/// </summary>
ACHIEVEMENT_INVALID = 0,
/// <summary>
/// 未完成
/// </summary>
ACHIEVEMENT_UNFINISHED = 1,
/// <summary>
/// 已完成
/// </summary>
ACHIEVEMENT_FINISHED = 2,
/// <summary>
/// 奖励已领取
/// </summary>
ACHIEVEMENT_POINT_TAKEN = 3,
}

View File

@@ -63,4 +63,4 @@ public enum ElementType
/// 默认 /// 默认
/// </summary> /// </summary>
Default = 255, Default = 255,
} }

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model;
/// <summary> /// <summary>
/// 简单的实现了 <see cref="INotifyPropertyChanged"/> 接口 /// 简单的实现了 <see cref="INotifyPropertyChanged"/> 接口
/// </summary> /// </summary>
public class Observable : INotifyPropertyChanged public abstract class Observable : INotifyPropertyChanged
{ {
/// <inheritdoc/> /// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
@@ -20,15 +20,17 @@ public class Observable : INotifyPropertyChanged
/// <param name="storage">现有值</param> /// <param name="storage">现有值</param>
/// <param name="value">新的值</param> /// <param name="value">新的值</param>
/// <param name="propertyName">属性名称</param> /// <param name="propertyName">属性名称</param>
protected void Set<T>([NotNullIfNotNull("value")] ref T storage, T value, [CallerMemberName] string propertyName = default!) /// <returns>项是否更新</returns>
protected bool Set<T>([NotNullIfNotNull("value")] ref T storage, T value, [CallerMemberName] string propertyName = default!)
{ {
if (Equals(storage, value)) if (Equals(storage, value))
{ {
return; return false;
} }
storage = value; storage = value;
OnPropertyChanged(propertyName); OnPropertyChanged(propertyName);
return true;
} }
/// <summary> /// <summary>

View File

@@ -9,7 +9,7 @@
<Identity <Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d" Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio" Publisher="CN=DGP Studio"
Version="1.0.26.0" /> Version="1.0.28.0" />
<Properties> <Properties>
<DisplayName>胡桃</DisplayName> <DisplayName>胡桃</DisplayName>

View File

@@ -29,6 +29,7 @@ public static class Program
[STAThread] [STAThread]
private static void Main(string[] args) private static void Main(string[] args)
{ {
_ = args;
XamlCheckProcessRequirements(); XamlCheckProcessRequirements();
ComWrappersSupport.InitializeComWrappers(); ComWrappersSupport.InitializeComWrappers();

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -1,6 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.Mvvm.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; namespace Snap.Hutao.Service.Achievement;
/// <summary> /// <summary>
@@ -9,5 +22,352 @@ namespace Snap.Hutao.Service.Achievement;
[Injection(InjectAs.Transient, typeof(IAchievementService))] [Injection(InjectAs.Transient, typeof(IAchievementService))]
internal class AchievementService : IAchievementService internal class AchievementService : IAchievementService
{ {
private readonly AppDbContext appDbContext;
private readonly ILogger<AchievementService> logger;
private readonly IMessenger messenger;
} private EntityArchive? currentArchive;
private ObservableCollection<EntityArchive>? archiveCollection;
/// <summary>
/// 构造一个新的成就服务
/// </summary>
/// <param name="appDbContext">数据库上下文</param>
/// <param name="logger">日志器</param>
/// <param name="messenger">消息器</param>
public AchievementService(AppDbContext appDbContext, ILogger<AchievementService> logger, IMessenger messenger)
{
this.appDbContext = appDbContext;
this.logger = logger;
this.messenger = messenger;
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
public ObservableCollection<EntityArchive> GetArchiveCollection()
{
return archiveCollection ??= new(appDbContext.AchievementArchives.ToList());
}
/// <inheritdoc/>
public List<BindingAchievement> GetAchievements(EntityArchive archive, IList<MetadataAchievement> metadata)
{
Guid archiveId = archive.InnerId;
List<EntityAchievement> entities = appDbContext.Achievements
.Where(a => a.ArchiveId == archiveId)
// Important! Prevent multiple sql command for SingleOrDefault below.
.ToList();
List<BindingAchievement> 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;
}
/// <inheritdoc/>
public ImportResult ImportFromUIAF(EntityArchive archive, List<UIAFItem> list, ImportOption option)
{
Guid archiveId = archive.InnerId;
switch (option)
{
case ImportOption.AggressiveMerge:
{
IOrderedEnumerable<UIAFItem> orederedUIAF = list.OrderBy(a => a.Id);
return MergeAchievements(archiveId, orederedUIAF, true);
}
case ImportOption.LazyMerge:
{
IOrderedEnumerable<UIAFItem> orederedUIAF = list.OrderBy(a => a.Id);
return MergeAchievements(archiveId, orederedUIAF, false);
}
case ImportOption.Overwrite:
{
IEnumerable<EntityAchievement> newData = list
.Select(uiaf => EntityAchievement.Create(archiveId, uiaf))
.OrderBy(a => a.Id);
return OverwriteAchievements(archiveId, newData);
}
default:
throw Must.NeverHappen();
}
}
/// <inheritdoc/>
public Task RemoveArchiveAsync(EntityArchive archive)
{
Must.NotNull(archiveCollection!);
// Sync cache
archiveCollection.Remove(archive);
// Sync database
appDbContext.AchievementArchives.Remove(archive);
return appDbContext.SaveChangesAsync();
}
/// <inheritdoc/>
public void SaveAchievements(EntityArchive archive, IList<BindingAchievement> achievements)
{
string name = archive.Name;
logger.LogInformation(EventIds.Achievement, "Begin saving achievements for [{name}]", name);
ValueStopwatch stopwatch = ValueStopwatch.StartNew();
IEnumerable<EntityAchievement> 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);
}
/// <inheritdoc/>
public async Task<ArchiveAddResult> 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<UIAFItem> orederedUIAF, bool aggressive)
{
IOrderedQueryable<EntityAchievement> oldData = appDbContext.Achievements
.Where(a => a.ArchiveId == archiveId)
.OrderBy(a => a.Id);
int add = 0;
int update = 0;
int remove = 0;
using (IEnumerator<EntityAchievement> entityEnumerator = oldData.GetEnumerator())
{
using (IEnumerator<UIAFItem> 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<EntityAchievement> newData)
{
IQueryable<EntityAchievement> oldData = appDbContext.Achievements
.Where(a => a.ArchiveId == archiveId)
.OrderBy(a => a.Id);
int add = 0;
int update = 0;
int remove = 0;
using (IEnumerator<EntityAchievement> oldDataEnumerator = oldData.GetEnumerator())
{
using (IEnumerator<EntityAchievement> 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();
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Achievement;
/// <summary>
/// 存档添加结果
/// </summary>
public enum ArchiveAddResult
{
/// <summary>
/// 添加成功
/// </summary>
Added,
/// <summary>
/// 名称无效
/// </summary>
InvalidName,
/// <summary>
/// 已经存在该用户
/// </summary>
AlreadyExists,
}

View File

@@ -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;
/// <summary> /// <summary>
/// 成就服务抽象 /// 成就服务抽象
/// </summary> /// </summary>
internal interface IAchievementService internal interface IAchievementService
{ {
/// <summary>
/// 当前存档
/// </summary>
EntityArchive? CurrentArchive { get; set; }
/// <summary>
/// 获取整合的成就
/// </summary>
/// <param name="archive">用户</param>
/// <param name="metadata">元数据</param>
/// <returns>整合的成就</returns>
List<BindingAchievement> GetAchievements(EntityArchive archive, IList<MetadataAchievement> metadata);
/// <summary>
/// 获取用于绑定的成就存档集合
/// </summary>
/// <returns>成就存档集合</returns>
ObservableCollection<EntityArchive> GetArchiveCollection();
/// <summary>
/// 导入UIAF数据
/// </summary>
/// <param name="archive">用户</param>
/// <param name="list">UIAF数据</param>
/// <param name="option">选项</param>
/// <returns>导入</returns>
ImportResult ImportFromUIAF(EntityArchive archive, List<UIAFItem> list, ImportOption option);
/// <summary>
/// 异步移除存档
/// </summary>
/// <param name="archive">待移除的存档</param>
/// <returns>任务</returns>
Task RemoveArchiveAsync(EntityArchive archive);
/// <summary>
/// 保存成就
/// </summary>
/// <param name="archive">用户</param>
/// <param name="achievements">成就</param>
void SaveAchievements(EntityArchive archive, IList<BindingAchievement> achievements);
/// <summary>
/// 尝试添加存档
/// </summary>
/// <param name="newArchive">新存档</param>
/// <returns>存档添加结果</returns>
Task<ArchiveAddResult> TryAddArchiveAsync(EntityArchive newArchive);
} }

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Achievement;
/// <summary>
/// 导入选项
/// </summary>
public enum ImportOption
{
/// <summary>
/// 贪婪合并
/// </summary>
AggressiveMerge = 0,
/// <summary>
/// 懒惰合并
/// </summary>
LazyMerge = 1,
/// <summary>
/// 完全覆盖
/// </summary>
Overwrite = 2,
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Achievement;
/// <summary>
/// 导入结果
/// </summary>
public struct ImportResult
{
/// <summary>
/// 新增数
/// </summary>
public readonly int Add;
/// <summary>
/// 更新数
/// </summary>
public readonly int Update;
/// <summary>
/// 移除数
/// </summary>
public readonly int Remove;
/// <summary>
/// 构造一个新的导入结果
/// </summary>
/// <param name="add">添加数</param>
/// <param name="update">更新数</param>
/// <param name="remove">移除数</param>
public ImportResult(int add, int update, int remove)
{
Add = add;
Update = update;
Remove = remove;
}
}

View File

@@ -12,7 +12,6 @@ internal class InfoBarService : IInfoBarService
{ {
private readonly TaskCompletionSource initializaionCompletionSource = new(); private readonly TaskCompletionSource initializaionCompletionSource = new();
private StackPanel? infoBarStack; private StackPanel? infoBarStack;
/// <inheritdoc/> /// <inheritdoc/>
public void Initialize(StackPanel container) public void Initialize(StackPanel container)

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Context.Database; using Snap.Hutao.Context.Database;
using Snap.Hutao.Model.Binding; using Snap.Hutao.Model.Binding;
using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Abstraction;
@@ -22,6 +23,7 @@ internal class UserService : IUserService
private readonly AppDbContext appDbContext; private readonly AppDbContext appDbContext;
private readonly UserClient userClient; private readonly UserClient userClient;
private readonly UserGameRoleClient userGameRoleClient; private readonly UserGameRoleClient userGameRoleClient;
private readonly IMessenger messenger;
private User? currentUser; private User? currentUser;
private ObservableCollection<User>? userCollection = null; private ObservableCollection<User>? userCollection = null;
@@ -32,11 +34,13 @@ internal class UserService : IUserService
/// <param name="appDbContext">应用程序数据库上下文</param> /// <param name="appDbContext">应用程序数据库上下文</param>
/// <param name="userClient">用户客户端</param> /// <param name="userClient">用户客户端</param>
/// <param name="userGameRoleClient">角色客户端</param> /// <param name="userGameRoleClient">角色客户端</param>
public UserService(AppDbContext appDbContext, UserClient userClient, UserGameRoleClient userGameRoleClient) /// <param name="messenger">消息器</param>
public UserService(AppDbContext appDbContext, UserClient userClient, UserGameRoleClient userGameRoleClient, IMessenger messenger)
{ {
this.appDbContext = appDbContext; this.appDbContext = appDbContext;
this.userClient = userClient; this.userClient = userClient;
this.userGameRoleClient = userGameRoleClient; this.userGameRoleClient = userGameRoleClient;
this.messenger = messenger;
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -45,6 +49,11 @@ internal class UserService : IUserService
get => currentUser; get => currentUser;
set set
{ {
if (currentUser == value)
{
return;
}
// only update when not processing a deletion // only update when not processing a deletion
if (value != null) if (value != null)
{ {
@@ -56,6 +65,8 @@ internal class UserService : IUserService
} }
} }
Message.UserChangedMessage message = new(currentUser, value);
// 当删除到无用户时也能正常反应状态 // 当删除到无用户时也能正常反应状态
currentUser = value; currentUser = value;
@@ -65,6 +76,8 @@ internal class UserService : IUserService
appDbContext.Users.Update(currentUser.Entity); appDbContext.Users.Update(currentUser.Entity);
appDbContext.SaveChanges(); appDbContext.SaveChanges();
} }
messenger.Send(message);
} }
} }
@@ -109,8 +122,10 @@ internal class UserService : IUserService
/// <inheritdoc/> /// <inheritdoc/>
public Task RemoveUserAsync(User user) public Task RemoveUserAsync(User user)
{ {
Must.NotNull(userCollection!);
// Sync cache // Sync cache
userCollection!.Remove(user); userCollection.Remove(user);
// Sync database // Sync database
appDbContext.Users.Remove(user.Entity); appDbContext.Users.Remove(user.Entity);
@@ -152,7 +167,7 @@ internal class UserService : IUserService
/// <inheritdoc/> /// <inheritdoc/>
public Task<User?> CreateUserAsync(string cookie) public Task<User?> CreateUserAsync(string cookie)
{ {
return User.CreateAsync(new() { Cookie = cookie }, userClient, userGameRoleClient); return User.CreateAsync(Model.Entity.User.Create(cookie), userClient, userGameRoleClient);
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -9,7 +9,9 @@
<RuntimeIdentifiers>win10-x64</RuntimeIdentifiers> <RuntimeIdentifiers>win10-x64</RuntimeIdentifiers>
<PublishProfile>win10-$(Platform).pubxml</PublishProfile> <PublishProfile>win10-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI> <UseWinUI>true</UseWinUI>
<ImplicitUsings>false</ImplicitUsings> <UseWPF>False</UseWPF>
<UseWindowsForms>False</UseWindowsForms>
<ImplicitUsings>False</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling> <EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<NeutralLanguage>zh-CN</NeutralLanguage> <NeutralLanguage>zh-CN</NeutralLanguage>
@@ -26,23 +28,20 @@
<StartupObject>Snap.Hutao.Program</StartupObject> <StartupObject>Snap.Hutao.Program</StartupObject>
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION;DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT</DefineConstants> <DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION;DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT</DefineConstants>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="Resource\Icon\UI_BagTabIcon_Avatar.png" /> <None Remove="Resource\Icon\UI_BagTabIcon_Avatar.png" />
<None Remove="Resource\Icon\UI_BagTabIcon_Weapon.png" /> <None Remove="Resource\Icon\UI_BagTabIcon_Weapon.png" />
<None Remove="Resource\Icon\UI_BtnIcon_ActivityEntry.png" /> <None Remove="Resource\Icon\UI_BtnIcon_ActivityEntry.png" />
<None Remove="Resource\Icon\UI_Icon_Achievement.png" /> <None Remove="Resource\Icon\UI_Icon_Achievement.png" />
<None Remove="Resource\Icon\UI_ItemIcon_201.png" />
<None Remove="Resource\Segoe Fluent Icons.ttf" /> <None Remove="Resource\Segoe Fluent Icons.ttf" />
<None Remove="stylecop.json" /> <None Remove="stylecop.json" />
<None Remove="View\Control\DescParamComboBox.xaml" /> <None Remove="View\Control\DescParamComboBox.xaml" />
<None Remove="View\Control\ItemIcon.xaml" /> <None Remove="View\Control\ItemIcon.xaml" />
<None Remove="View\Control\SkillPivot.xaml" /> <None Remove="View\Control\SkillPivot.xaml" />
<None Remove="View\Dialog\AchievementArchiveCreateDialog.xaml" />
<None Remove="View\Dialog\AchievementImportDialog.xaml" />
<None Remove="View\Dialog\UserDialog.xaml" /> <None Remove="View\Dialog\UserDialog.xaml" />
<None Remove="View\MainView.xaml" /> <None Remove="View\MainView.xaml" />
<None Remove="View\Page\AchievementPage.xaml" /> <None Remove="View\Page\AchievementPage.xaml" />
@@ -71,6 +70,7 @@
<Content Include="Resource\Icon\UI_BagTabIcon_Weapon.png" /> <Content Include="Resource\Icon\UI_BagTabIcon_Weapon.png" />
<Content Include="Resource\Icon\UI_BtnIcon_ActivityEntry.png" /> <Content Include="Resource\Icon\UI_BtnIcon_ActivityEntry.png" />
<Content Include="Resource\Icon\UI_Icon_Achievement.png" /> <Content Include="Resource\Icon\UI_Icon_Achievement.png" />
<Content Include="Resource\Icon\UI_ItemIcon_201.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -78,20 +78,19 @@
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" /> <PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Behaviors" Version="7.1.2" /> <PackageReference Include="CommunityToolkit.WinUI.UI.Behaviors" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" /> <PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Media" Version="7.1.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" /> <!-- The PrivateAssets & IncludeAssets of Microsoft.EntityFrameworkCore.Tools should be remove to prevent multiple deps files-->
<!-- The PrivateAssets & IncludeAssets of Microsoft.EntityFrameworkCore.Tools should be remove to prevent multiple deps file--> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.7" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.2.32" /> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.3.44">
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.2.32">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.0.64" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.1" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.1" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.3" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435"> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -114,6 +113,16 @@
<ItemGroup> <ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" /> <None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\AchievementArchiveCreateDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\AchievementImportDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="View\Control\DescParamComboBox.xaml"> <Page Update="View\Control\DescParamComboBox.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>

View File

@@ -0,0 +1,22 @@
<ContentDialog
x:Class="Snap.Hutao.View.Dialog.AchievementArchiveCreateDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Snap.Hutao.View.Dialog"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="设置成就存档的名称"
DefaultButton="Primary"
PrimaryButtonText="确认"
CloseButtonText="取消"
Style="{StaticResource DefaultContentDialogStyle}">
<Grid>
<TextBox
Margin="0,0,0,8"
x:Name="InputText"
PlaceholderText="在此处输入"
VerticalAlignment="Top"/>
</Grid>
</ContentDialog>

View File

@@ -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;
/// <summary>
/// 成就存档创建对话框
/// </summary>
public sealed partial class AchievementArchiveCreateDialog : ContentDialog
{
/// <summary>
/// 构造一个新的成就存档创建对话框
/// </summary>
/// <param name="window">窗体</param>
public AchievementArchiveCreateDialog(Window window)
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
}
/// <summary>
/// 获取输入的字符串
/// </summary>
/// <returns>输入的结果</returns>
public async Task<Result<bool, string>> GetInputAsync()
{
ContentDialogResult result = await ShowAsync();
string text = InputText.Text ?? string.Empty;
return new(result == ContentDialogResult.Primary, text);
}
}

View File

@@ -0,0 +1,78 @@
<ContentDialog
x:Class="Snap.Hutao.View.Dialog.AchievementImportDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
mc:Ignorable="d"
Title="为当前存档导入成就"
DefaultButton="Primary"
PrimaryButtonText="确认"
CloseButtonText="取消"
Style="{StaticResource DefaultContentDialogStyle}">
<mxi:Interaction.Behaviors>
<shcb:ContentDialogBehavior/>
</mxi:Interaction.Behaviors>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<cwuc:UniformGrid
Grid.Row="0"
Columns="3"
ColumnSpacing="16"
RowSpacing="16">
<cwuc:HeaderedContentControl Header="导出App">
<TextBlock
Style="{StaticResource CaptionTextBlockStyle}"
Opacity="0.6"
Margin="0,4,0,0"
Text="{x:Bind UIAF.Info.ExportApp,Mode=OneWay,TargetNullValue=未知}"/>
</cwuc:HeaderedContentControl>
<cwuc:HeaderedContentControl Header="导出时间">
<TextBlock
Style="{StaticResource CaptionTextBlockStyle}"
Opacity="0.6"
Margin="0,4,0,0"
Text="{x:Bind UIAF.Info.ExportDateTime.LocalDateTime,Mode=OneWay,TargetNullValue=未知}"/>
</cwuc:HeaderedContentControl>
<cwuc:HeaderedContentControl Header="导出App版本">
<TextBlock
Style="{StaticResource CaptionTextBlockStyle}"
Opacity="0.6"
Margin="0,4,0,0"
Text="{x:Bind UIAF.Info.ExportAppVersion,Mode=OneWay,TargetNullValue=未知}"/>
</cwuc:HeaderedContentControl>
<cwuc:HeaderedContentControl Header="UIAF 版本">
<TextBlock
Style="{StaticResource CaptionTextBlockStyle}"
Opacity="0.6"
Margin="0,4,0,0"
Text="{x:Bind UIAF.Info.UIAFVersion,Mode=OneWay,TargetNullValue=未知}"/>
</cwuc:HeaderedContentControl>
<cwuc:HeaderedContentControl Header="成就个数">
<TextBlock
Style="{StaticResource CaptionTextBlockStyle}"
Opacity="0.6"
Margin="0,4,0,0"
Text="{x:Bind UIAF.List.Count,Mode=OneWay,TargetNullValue=未知}"/>
</cwuc:HeaderedContentControl>
</cwuc:UniformGrid>
<RadioButtons
Name="ImportModeSelector"
Header="导入模式"
Grid.Row="1"
Margin="0,16,0,0"
SelectedIndex="0">
<RadioButton Content="贪婪(添加新数据,更新已完成项)"/>
<RadioButton Content="懒惰(添加新数据,跳过已完成项)"/>
<RadioButton Content="覆盖(删除老数据,添加新的数据)"/>
</RadioButtons>
</Grid>
</ContentDialog>

View File

@@ -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;
/// <summary>
/// 成就对话框
/// </summary>
public sealed partial class AchievementImportDialog : ContentDialog
{
private static readonly DependencyProperty UIAFProperty = Property<AchievementImportDialog>.Depend(nameof(UIAF), default(UIAF));
/// <summary>
/// 构造一个新的成就对话框
/// </summary>
/// <param name="window">呈现的父窗口</param>
/// <param name="uiaf">uiaf数据</param>
public AchievementImportDialog(Window window, UIAF uiaf)
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
UIAF = uiaf;
}
/// <summary>
/// UIAF数据
/// </summary>
public UIAF UIAF
{
get { return (UIAF)GetValue(UIAFProperty); }
set { SetValue(UIAFProperty, value); }
}
/// <summary>
/// 异步获取导入选项
/// </summary>
/// <returns>导入选项</returns>
public async Task<Result<bool, ImportOption>> GetImportOptionAsync()
{
ContentDialogResult result = await ShowAsync();
ImportOption option = (ImportOption)ImportModeSelector.SelectedIndex;
return new Result<bool, ImportOption>(result == ContentDialogResult.Primary, option);
}
}

View File

@@ -22,13 +22,53 @@
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/> <shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors> </mxi:Interaction.Behaviors>
<Grid> <Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<CommandBar
Grid.Row="0"
DefaultLabelPosition="Right">
<CommandBar.Background>
<SolidColorBrush Color="{ThemeResource CardBackgroundFillColorSecondary}"/>
</CommandBar.Background>
<AppBarElementContainer>
<ComboBox
MinWidth="160"
Height="36"
Margin="2,6,3,6"
DisplayMemberPath="Name"
ItemsSource="{Binding Archives,Mode=OneWay}"
SelectedItem="{Binding SelectedArchive,Mode=TwoWay}"/>
</AppBarElementContainer>
<CommandBar.SecondaryCommands>
<AppBarButton
Icon="Add"
Label="创建新存档"
Command="{Binding AddArchiveCommand}"/>
<AppBarButton
Icon="Delete"
Label="删除当前存档"
Command="{Binding RemoveArchiveCommand}"/>
<AppBarSeparator/>
<AppBarButton
Icon="Paste"
Label="从剪贴板导入"
Command="{Binding ImportUIAFFromClipboardCommand}"/>
<AppBarButton
Icon="OpenFile"
Label="从UIAF文件导入"
Command="{Binding ImportUIAFFromFileCommand}"/>
</CommandBar.SecondaryCommands>
</CommandBar>
<SplitView <SplitView
Grid.Row="1"
IsPaneOpen="True" IsPaneOpen="True"
DisplayMode="Inline" DisplayMode="Inline"
OpenPaneLength="252"> OpenPaneLength="252"
<SplitView.PaneBackground> PaneBackground="Transparent">
<SolidColorBrush Color="{ThemeResource CardBackgroundFillColorSecondary}"/>
</SplitView.PaneBackground>
<SplitView.Pane> <SplitView.Pane>
<ListView <ListView
SelectionMode="Single" SelectionMode="Single"
@@ -62,8 +102,7 @@
<ItemsControl <ItemsControl
Margin="16,0,0,16" Margin="16,0,0,16"
ItemsSource="{Binding Achievements}"> ItemsSource="{Binding Achievements}">
<!--ContentThemeTransition here can make items blinking, <!--ContentThemeTransition here can make items blinking, cause we are using ItemsStackPanel-->
cause we are using ItemsStackPanel-->
<!--<ItemsControl.Transitions> <!--<ItemsControl.Transitions>
<ContentThemeTransition/> <ContentThemeTransition/>
</ItemsControl.Transitions>--> </ItemsControl.Transitions>-->
@@ -86,28 +125,40 @@
MinHeight="48"> MinHeight="48">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<!-- Icon -->
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<!-- Header and subtitle -->
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<!-- Action control -->
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<CheckBox <CheckBox
IsChecked="{Binding IsChecked,Mode=TwoWay}"
Margin="6,0,0,0" Margin="6,0,0,0"
Style="{StaticResource DefaultCheckBoxStyle}" Style="{StaticResource DefaultCheckBoxStyle}"
Padding="16,0,0,0" Padding="16,0,0,0"
Grid.Column="1"> Grid.Column="1">
<CheckBox.Content> <CheckBox.Content>
<StackPanel> <StackPanel>
<TextBlock Text="{Binding Title}"/> <TextBlock Text="{Binding Inner.Title}"/>
<TextBlock <TextBlock
Margin="0,2,0,0" Margin="0,2,0,0"
Style="{StaticResource SecondaryTextStyle}" Style="{StaticResource SecondaryTextStyle}"
Text="{Binding Description}"/> Text="{Binding Inner.Description}"/>
</StackPanel> </StackPanel>
</CheckBox.Content> </CheckBox.Content>
</CheckBox> </CheckBox>
<Grid Grid.Column="3">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="32"/>
</Grid.ColumnDefinitions>
<Image
Height="32"
Source="ms-appx:///Resource/Icon/UI_ItemIcon_201.png"/>
<TextBlock
Margin="12,0,0,0"
VerticalAlignment="Center"
Grid.Column="1"
Text="{Binding Inner.FinishReward.Count}"/>
</Grid>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
@@ -115,6 +166,5 @@
</ScrollViewer> </ScrollViewer>
</SplitView.Content> </SplitView.Content>
</SplitView> </SplitView>
</Grid> </Grid>
</shcc:CancellablePage> </shcc:CancellablePage>

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml.Navigation; using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Control.Cancellable; using Snap.Hutao.Control.Cancellable;
using Snap.Hutao.Service.Navigation; using Snap.Hutao.Service.Navigation;
@@ -32,4 +33,12 @@ public sealed partial class AchievementPage : CancellablePage
extra.NotifyNavigationCompleted(); extra.NotifyNavigationCompleted();
} }
} }
/// <inheritdoc/>
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
((AchievementViewModel)DataContext).SaveAchievements();
base.OnNavigatingFrom(e);
}
} }

View File

@@ -3,7 +3,6 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Navigation; using Microsoft.UI.Xaml.Navigation;
using Microsoft.VisualStudio.Threading;
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Extension; using Snap.Hutao.Extension;
using Snap.Hutao.Service.Navigation; using Snap.Hutao.Service.Navigation;

View File

@@ -1,9 +1,8 @@
<UserControl <UserControl
x:Class="Snap.Hutao.View.TitleView" x:Class="Snap.Hutao.View.TitleView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Snap.Hutao.View"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" mc:Ignorable="d"
VerticalAlignment="Top" VerticalAlignment="Top"

View File

@@ -2,12 +2,28 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI.UI; using CommunityToolkit.WinUI.UI;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control;
using Snap.Hutao.Control.Cancellable; using Snap.Hutao.Control.Cancellable;
using Snap.Hutao.Extension;
using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Message;
using Snap.Hutao.Model.InterChange.Achievement;
using Snap.Hutao.Model.Metadata.Achievement; using Snap.Hutao.Model.Metadata.Achievement;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Achievement;
using Snap.Hutao.Service.Metadata; using Snap.Hutao.Service.Metadata;
using Snap.Hutao.View.Dialog;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
namespace Snap.Hutao.ViewModel; namespace Snap.Hutao.ViewModel;
@@ -15,27 +31,86 @@ namespace Snap.Hutao.ViewModel;
/// 成就视图模型 /// 成就视图模型
/// </summary> /// </summary>
[Injection(InjectAs.Transient)] [Injection(InjectAs.Transient)]
internal class AchievementViewModel : ObservableObject, ISupportCancellation internal class AchievementViewModel
: ObservableObject,
ISupportCancellation,
IRecipient<AchievementArchiveChangedMessage>,
IRecipient<MainWindowClosedMessage>
{ {
private readonly IMetadataService metadataService; private readonly IMetadataService metadataService;
private readonly IAchievementService achievementService;
private readonly IInfoBarService infoBarService;
private readonly JsonSerializerOptions options;
private readonly IPickerFactory pickerFactory;
private AdvancedCollectionView? achievements; private AdvancedCollectionView? achievements;
private IList<AchievementGoal>? achievementGoals; private IList<AchievementGoal>? achievementGoals;
private AchievementGoal? selectedAchievementGoal; private AchievementGoal? selectedAchievementGoal;
private ObservableCollection<Model.Entity.AchievementArchive>? archives;
private Model.Entity.AchievementArchive? selectedArchive;
/// <summary> /// <summary>
/// 构造一个新的成就视图模型 /// 构造一个新的成就视图模型
/// </summary> /// </summary>
/// <param name="metadataService">元数据服务</param> /// <param name="metadataService">元数据服务</param>
/// <param name="achievementService">成就服务</param>
/// <param name="infoBarService">信息条服务</param>
/// <param name="options">Json序列化选项</param>
/// <param name="asyncRelayCommandFactory">异步命令工厂</param> /// <param name="asyncRelayCommandFactory">异步命令工厂</param>
public AchievementViewModel(IMetadataService metadataService, IAsyncRelayCommandFactory asyncRelayCommandFactory) /// <param name="pickerFactory">文件选择器工厂</param>
/// <param name="messenger">消息器</param>
public AchievementViewModel(
IMetadataService metadataService,
IAchievementService achievementService,
IInfoBarService infoBarService,
JsonSerializerOptions options,
IAsyncRelayCommandFactory asyncRelayCommandFactory,
IPickerFactory pickerFactory,
IMessenger messenger)
{ {
this.metadataService = metadataService; this.metadataService = metadataService;
this.achievementService = achievementService;
this.infoBarService = infoBarService;
this.options = options;
this.pickerFactory = pickerFactory;
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync); OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
ImportUIAFFromClipboardCommand = asyncRelayCommandFactory.Create(ImportUIAFFromClipboardAsync);
ImportUIAFFromFileCommand = asyncRelayCommandFactory.Create(ImportUIAFFromFileAsync);
AddArchiveCommand = asyncRelayCommandFactory.Create(AddArchiveAsync);
RemoveArchiveCommand = asyncRelayCommandFactory.Create(RemoveArchiveAsync);
messenger.Register<AchievementArchiveChangedMessage>(this);
messenger.Register<MainWindowClosedMessage>(this);
} }
/// <inheritdoc/> /// <inheritdoc/>
public CancellationToken CancellationToken { get; set; } public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 成就存档集合
/// </summary>
public ObservableCollection<Model.Entity.AchievementArchive>? Archives
{
get => archives;
set => SetProperty(ref archives, value);
}
/// <summary>
/// 选中的成就存档
/// </summary>
public Model.Entity.AchievementArchive? SelectedArchive
{
get => selectedArchive;
set
{
if (SetProperty(ref selectedArchive, value))
{
achievementService.CurrentArchive = value;
}
}
}
/// <summary> /// <summary>
/// 成就视图 /// 成就视图
/// </summary> /// </summary>
@@ -63,7 +138,7 @@ internal class AchievementViewModel : ObservableObject, ISupportCancellation
set set
{ {
SetProperty(ref selectedAchievementGoal, value); SetProperty(ref selectedAchievementGoal, value);
OnGoalChanged(value); UpdateAchievementFilter(value);
} }
} }
@@ -72,21 +147,249 @@ internal class AchievementViewModel : ObservableObject, ISupportCancellation
/// </summary> /// </summary>
public ICommand OpenUICommand { get; } public ICommand OpenUICommand { get; }
/// <summary>
/// 添加存档命令
/// </summary>
public ICommand AddArchiveCommand { get; }
/// <summary>
/// 删除存档命令
/// </summary>
public ICommand RemoveArchiveCommand { get; }
/// <summary>
/// 从剪贴板导入UIAF命令
/// </summary>
public ICommand ImportUIAFFromClipboardCommand { get; }
/// <summary>
/// 从文件导入UIAF命令
/// </summary>
public ICommand ImportUIAFFromFileCommand { get; }
/// <inheritdoc/>
public void Receive(MainWindowClosedMessage message)
{
SaveAchievements();
}
/// <inheritdoc/>
public void Receive(AchievementArchiveChangedMessage message)
{
HandleArchiveChangeAsync(message.OldValue, message.NewValue).SafeForget();
}
/// <summary>
/// 保存当前用户的成就
/// </summary>
public void SaveAchievements()
{
if (Achievements != null && SelectedArchive != null)
{
achievementService.SaveAchievements(SelectedArchive, (Achievements.Source as IList<Model.Binding.Achievement>)!);
}
}
private async Task HandleArchiveChangeAsync(Model.Entity.AchievementArchive? oldArchieve, Model.Entity.AchievementArchive? newArchieve)
{
if (oldArchieve != null && Achievements != null)
{
achievementService.SaveAchievements(oldArchieve, (Achievements.Source as IList<Model.Binding.Achievement>)!);
}
if (newArchieve != null)
{
await UpdateAchievementsAsync(newArchieve);
}
else
{
infoBarService.Warning("请创建或选择一个成就存档");
}
}
private async Task OpenUIAsync() private async Task OpenUIAsync()
{ {
if (await metadataService.InitializeAsync(CancellationToken)) if (await metadataService.InitializeAsync(CancellationToken))
{ {
Achievements = new(await metadataService.GetAchievementsAsync(CancellationToken), true);
AchievementGoals = await metadataService.GetAchievementGoalsAsync(CancellationToken); 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<Achievement> rawAchievements = await metadataService.GetAchievementsAsync(CancellationToken);
List<Model.Binding.Achievement> 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<UIAF>(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<UIAF>(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) if (Achievements != null)
{ {
Achievements.Filter = goal != 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); : ((object o) => true);
} }
} }

View File

@@ -123,13 +123,12 @@ internal class UserViewModel : ObservableObject
private async Task AddUserAsync() private async Task AddUserAsync()
{ {
// Get cookie from user input // Get cookie from user input
Window mainWindow = Ioc.Default.GetRequiredService<MainWindow>(); (bool isOk, string cookie) = await new UserDialog(App.Window!).GetInputCookieAsync();
Result<bool, string> result = await new UserDialog(mainWindow).GetInputCookieAsync();
// User confirms the input // User confirms the input
if (result.IsOk) if (isOk)
{ {
if (TryValidateCookie(userService.ParseCookie(result.Value), out IDictionary<string, string>? filteredCookie)) if (TryValidateCookie(userService.ParseCookie(cookie), out IDictionary<string, string>? filteredCookie))
{ {
string simplifiedCookie = string.Join(';', filteredCookie.Select(kvp => $"{kvp.Key}={kvp.Value}")); string simplifiedCookie = string.Join(';', filteredCookie.Select(kvp => $"{kvp.Key}={kvp.Value}"));

View File

@@ -28,7 +28,6 @@ internal static class HttpClientDynamicSecretExtensions
/// <param name="httpClient">请求器</param> /// <param name="httpClient">请求器</param>
/// <param name="options">选项</param> /// <param name="options">选项</param>
/// <param name="url">地址</param> /// <param name="url">地址</param>
/// <param name="data">post数据</param>
/// <returns>响应</returns> /// <returns>响应</returns>
public static IDynamicSecret2HttpClient UsingDynamicSecret2(this HttpClient httpClient, JsonSerializerOptions options, string url) public static IDynamicSecret2HttpClient UsingDynamicSecret2(this HttpClient httpClient, JsonSerializerOptions options, string url)
{ {
@@ -47,7 +46,6 @@ internal static class HttpClientDynamicSecretExtensions
public static IDynamicSecret2HttpClient<TValue> UsingDynamicSecret2<TValue>(this HttpClient httpClient, JsonSerializerOptions options, string url, TValue data) public static IDynamicSecret2HttpClient<TValue> UsingDynamicSecret2<TValue>(this HttpClient httpClient, JsonSerializerOptions options, string url, TValue data)
where TValue : class where TValue : class
{ {
httpClient.DefaultRequestHeaders.Set("DS", DynamicSecretProvider2.Create(options, url, data));
return new DynamicSecret2HttpClient<TValue>(httpClient, options, url, data); return new DynamicSecret2HttpClient<TValue>(httpClient, options, url, data);
} }
} }

View File

@@ -6,6 +6,9 @@ using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward; namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward;
/// <summary>
/// 奖励
/// </summary>
public class Reward public class Reward
{ {
/// <summary> /// <summary>

View File

@@ -7,35 +7,36 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.BbsSignReward;
/// <summary> /// <summary>
/// 签到结果 /// 签到结果
/// https://docs.geetest.com/sensebot/apirefer/api/server
/// </summary> /// </summary>
public class SignInResult public class SignInResult
{ {
/// <summary> /// <summary>
/// 通常是 "" ///
/// </summary> /// </summary>
[JsonPropertyName("code")] [JsonPropertyName("code")]
public string Code { get; set; } = default!; public string Code { get; set; } = default!;
/// <summary> /// <summary>
/// 通常是 "" /// 风控码 375
/// </summary> /// </summary>
[JsonPropertyName("risk_code")] [JsonPropertyName("risk_code")]
public int RiskCode { get; set; } public int RiskCode { get; set; }
/// <summary> /// <summary>
/// 通常是 "" /// geetest appid
/// </summary> /// </summary>
[JsonPropertyName("gt")] [JsonPropertyName("gt")]
public string Gt { get; set; } = default!; public string Gt { get; set; } = default!;
/// <summary> /// <summary>
/// 通常是 "" /// geetest challenge id
/// </summary> /// </summary>
[JsonPropertyName("challenge")] [JsonPropertyName("challenge")]
public string Challenge { get; set; } = default!; public string Challenge { get; set; } = default!;
/// <summary> /// <summary>
/// 通常是 "" /// geetest 服务状态
/// </summary> /// </summary>
[JsonPropertyName("success")] [JsonPropertyName("success")]
public int Success { get; set; } public int Success { get; set; }