file nesting

This commit is contained in:
DismissedLight
2022-11-09 12:08:59 +08:00
parent 9a3183e917
commit 9667917559
70 changed files with 1953 additions and 490 deletions

View File

@@ -0,0 +1,31 @@
{
"help": "https://go.microsoft.com/fwlink/?linkid=866610",
"dependentFileProviders": {
"add": {
"extensionToExtension": {
"add": {
".json": [ ".txt" ]
}
},
"pathSegment": {
"add": {
".*": [ ".cs" ]
}
},
"fileSuffixToExtension": {
"add": {
"DesignTimeFactory.cs": [".cs"]
}
},
"fileToFile": {
"add": {
"app.manifest": [ "App.xaml.cs" ],
"Package.appxmanifest": [ "App.xaml.cs" ],
"GlobalUsing.cs": [ "Program.cs" ],
".filenesting.json": [ "Program.cs" ],
".editorconfig": [ "Program.cs" ]
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -61,6 +61,11 @@ public class AppDbContext : DbContext
/// </summary>
public DbSet<GameAccount> GameAccounts { get; set; } = default!;
/// <summary>
/// 实时便笺
/// </summary>
public DbSet<DailyNoteEntry> DailyNotes { get; set; } = default!;
/// <summary>
/// 构造一个临时的应用程序数据库上下文
/// </summary>
@@ -76,6 +81,7 @@ public class AppDbContext : DbContext
{
modelBuilder
.ApplyConfiguration(new AvatarInfoConfiguration())
.ApplyConfiguration(new UserConfiguration());
.ApplyConfiguration(new UserConfiguration())
.ApplyConfiguration(new DailyNoteEntryConfiguration());
}
}

View File

@@ -15,12 +15,12 @@ namespace Snap.Hutao.Core;
/// </summary>
internal static class CoreEnvironment
{
// 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
// 计算过程https://github.com/UIGF-org/Hoyolab.Salt
/// <summary>
/// 动态密钥1的盐
/// </summary>
public const string DynamicSecret1Salt = "yUZ3s0Sna1IrSNfk29Vo6vRapdOyqyhB";
public const string DynamicSecret1Salt = "jEpJb9rRARU2rXDA9qYbZ3selxkuct9a";
/// <summary>
/// 动态密钥2的盐
@@ -35,7 +35,7 @@ internal static class CoreEnvironment
/// <summary>
/// 米游社 Rpc 版本
/// </summary>
public const string HoyolabXrpcVersion = "2.38.1";
public const string HoyolabXrpcVersion = "2.40.1";
/// <summary>
/// 标准UA

View File

@@ -0,0 +1,38 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Win32.TaskScheduler;
using SchedulerTask = Microsoft.Win32.TaskScheduler.Task;
namespace Snap.Hutao.Core;
/// <summary>
/// 任务计划器服务
/// </summary>
internal class TaskSchedulerHelper
{
private const string DailyNoteRefreshTaskName = "SnapHutaoDailyNoteRefreshTask";
/// <summary>
/// 注册实时便笺刷新任务
/// </summary>
/// <param name="interval">间隔(秒)</param>
public void RegisterForDailyNoteRefresh(int interval)
{
TimeSpan intervalTime = TimeSpan.FromSeconds(interval);
if (TaskService.Instance.GetTask(DailyNoteRefreshTaskName) is SchedulerTask targetTask)
{
TimeTrigger? trigger = targetTask.Definition.Triggers[0] as TimeTrigger;
trigger!.Repetition.Interval = intervalTime;
targetTask.RegisterChanges();
}
else
{
TaskDefinition task = TaskService.Instance.NewTask();
task.RegistrationInfo.Description = "胡桃实时便笺刷新任务 | 请勿编辑或删除。";
task.Triggers.Add(new TimeTrigger() { Repetition = new(intervalTime, TimeSpan.Zero), });
task.Actions.Add("explorer", "hutao://DailyNote/Refresh");
TaskService.Instance.RootFolder.RegisterTaskDefinition(DailyNoteRefreshTaskName, task);
}
}
}

View File

@@ -1,25 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Metadata.Achievement;
namespace Snap.Hutao.Core.Windowing;
/// <summary>
/// 成就触发器类型
/// 背景类型
/// </summary>
public enum AchievementTriggerType
public enum BackdropType
{
/// <summary>
/// 任务
///
/// </summary>
Quest = 1,
None = 0,
/// <summary>
/// 子任务
/// 亚克力
/// </summary>
SubQuest = 2,
Acrylic,
/// <summary>
/// 日常任务
/// 云母
/// </summary>
DailyTask = 3,
Mica,
/// <summary>
/// 变种云母
/// </summary>
MicaAlt,
}

View File

@@ -1,12 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension;
using Snap.Hutao.Message;
using Snap.Hutao.Win32;
using System.IO;
using Windows.ApplicationModel;
using Windows.Graphics;
using Windows.UI;
using Windows.Win32.Foundation;
@@ -15,11 +19,10 @@ using WinRT.Interop;
namespace Snap.Hutao.Core.Windowing;
/// <summary>
/// 窗口管理器
/// 主要包含了针对窗体的 P/Inoke 逻辑
/// 扩展窗口
/// </summary>
/// <typeparam name="TWindow">窗体类型</typeparam>
internal sealed class ExtendedWindow<TWindow>
internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMessage>
where TWindow : Window, IExtendedWindowSource
{
private readonly HWND handle;
@@ -33,8 +36,10 @@ internal sealed class ExtendedWindow<TWindow>
private readonly bool useLegacyDragBar;
private SystemBackdrop? systemBackdrop;
/// <summary>
/// 构造一个新的窗口状态管理器
/// 构造一个新的扩展窗口
/// </summary>
/// <param name="window">窗口</param>
/// <param name="titleBar">充当标题栏的元素</param>
@@ -65,6 +70,17 @@ internal sealed class ExtendedWindow<TWindow>
return new(window, window.TitleBar);
}
/// <inheritdoc/>
public void Receive(BackdropTypeChangedMessage message)
{
if (systemBackdrop != null)
{
systemBackdrop.BackdropType = message.BackdropType;
bool micaApplied = systemBackdrop.TryApply();
logger.LogInformation(EventIds.BackdropState, "Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed");
}
}
private static void UpdateTitleButtonColor(AppWindowTitleBar appTitleBar)
{
appTitleBar.ButtonBackgroundColor = Colors.Transparent;
@@ -102,7 +118,7 @@ internal sealed class ExtendedWindow<TWindow>
private void InitializeWindow()
{
appWindow.Title = "胡桃";
appWindow.SetIcon(Path.Combine(Package.Current.InstalledLocation.Path, "Assets/Logos/Logo.ico"));
ExtendsContentIntoTitleBar();
Persistence.RecoverOrInit(appWindow, window.PersistSize, window.InitSize);
@@ -113,12 +129,14 @@ internal sealed class ExtendedWindow<TWindow>
appWindow.Show(true);
bool micaApplied = new SystemBackdrop(window).TryApply();
systemBackdrop = new(window);
bool micaApplied = systemBackdrop.TryApply();
logger.LogInformation(EventIds.BackdropState, "Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed");
bool subClassApplied = subclassManager.TrySetWindowSubclass();
logger.LogInformation(EventIds.SubClassing, "Apply {name} : {result}", nameof(WindowSubclassManager<TWindow>), subClassApplied ? "succeed" : "failed");
Ioc.Default.GetRequiredService<IMessenger>().Register(this);
window.Closed += OnWindowClosed;
}

View File

@@ -1,9 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using System.Runtime.InteropServices;
using Windows.System;
using WinRT;
@@ -18,7 +22,7 @@ public class SystemBackdrop
private readonly Window window;
private DispatcherQueueHelper? dispatcherQueueHelper;
private MicaController? backdropController;
private ISystemBackdropControllerWithTargets? backdropController;
private SystemBackdropConfiguration? configuration;
/// <summary>
@@ -28,7 +32,18 @@ public class SystemBackdrop
public SystemBackdrop(Window window)
{
this.window = window;
using (IServiceScope scope = Ioc.Default.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
SettingEntry entry = appDbContext.Settings.SingleOrAdd(SettingEntry.SystemBackdropType, BackdropType.Mica.ToString());
BackdropType = Enum.Parse<BackdropType>(entry.Value!);
}
}
/// <summary>
/// 背景类型
/// </summary>
public BackdropType BackdropType { get; set; }
/// <summary>
/// 尝试设置背景
@@ -36,30 +51,49 @@ public class SystemBackdrop
/// <returns>是否设置成功</returns>
public bool TryApply()
{
if (!MicaController.IsSupported())
bool isSupport = BackdropType switch
{
BackdropType.Acrylic => DesktopAcrylicController.IsSupported(),
BackdropType.Mica or BackdropType.MicaAlt => MicaController.IsSupported(),
_ => false,
};
if (!isSupport)
{
return false;
}
else
{
// Previous one
if (backdropController != null)
{
backdropController.RemoveAllSystemBackdropTargets();
}
else
{
dispatcherQueueHelper = new();
dispatcherQueueHelper.Ensure();
}
// Hooking up the policy object
configuration = new();
configuration = new()
{
IsInputActive = true, // Initial configuration state.
};
SetConfigurationSourceTheme(configuration);
window.Activated += OnWindowActivated;
window.Closed += OnWindowClosed;
((FrameworkElement)window.Content).ActualThemeChanged += OnWindowThemeChanged;
// Initial configuration state.
configuration.IsInputActive = true;
SetConfigurationSourceTheme(configuration);
backdropController = new()
backdropController = BackdropType switch
{
// Mica Alt
Kind = MicaKind.BaseAlt,
BackdropType.Acrylic => new DesktopAcrylicController(),
BackdropType.Mica => new MicaController() { Kind = MicaKind.Base },
BackdropType.MicaAlt => new MicaController() { Kind = MicaKind.BaseAlt },
_ => throw Must.NeverHappen(),
};
backdropController.AddSystemBackdropTarget(window.As<ICompositionSupportsSystemBackdrop>());
backdropController.SetSystemBackdropConfiguration(configuration);
@@ -69,7 +103,7 @@ public class SystemBackdrop
private void OnWindowActivated(object sender, WindowActivatedEventArgs args)
{
Must.NotNull(configuration!).IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
configuration!.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
}
private void OnWindowClosed(object sender, WindowEventArgs args)

View File

@@ -52,9 +52,10 @@ internal class WindowSubclassManager<TWindow> : IDisposable
bool titleBarHooked = true;
// only hook up drag bar proc when use legacy Window.ExtendsContentIntoTitleBar
// only hook up drag bar proc when not use legacy Window.ExtendsContentIntoTitleBar
if (isLegacyDragBar)
{
titleBarHooked = false;
hwndDragBar = FindWindowEx(hwnd, default, "DRAG_BAR_WINDOW_CLASS", string.Empty);
if (!hwndDragBar.IsNull)
@@ -90,6 +91,12 @@ internal class WindowSubclassManager<TWindow> : IDisposable
window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
break;
}
case WM_NCRBUTTONDOWN:
case WM_NCRBUTTONUP:
{
return new(0);
}
}
return DefSubclassProc(hwnd, uMsg, wParam, lParam);

View File

@@ -0,0 +1,34 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.ObjectModel;
namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="Collection{T}"/> 部分
/// </summary>
public static partial class EnumerableExtension
{
/// <summary>
/// 移除集合中满足条件的项
/// </summary>
/// <typeparam name="T">集合项类型</typeparam>
/// <param name="collection">集合</param>
/// <param name="shouldRemovePredicate">是否应当移除</param>
/// <returns>移除的个数</returns>
public static int RemoveWhere<T>(this Collection<T> collection, Func<T, bool> shouldRemovePredicate)
{
int count = 0;
foreach (T item in collection.ToList())
{
if (shouldRemovePredicate.Invoke(item))
{
collection.Remove(item);
count++;
}
}
return count;
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="Dictionary{TKey, TValue}"/> 部分
/// </summary>
public static partial class EnumerableExtension
{
/// <summary>
/// 获取值或默认值
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <typeparam name="TValue">值类型</typeparam>
/// <param name="dictionary">字典</param>
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>结果值</returns>
public static TValue? GetValueOrDefault2<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue? defaultValue = default)
where TKey : notnull
{
if (dictionary.TryGetValue(key, out TValue? value))
{
return value;
}
return defaultValue;
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
public static void Increase<TKey>(this Dictionary<TKey, int> dict, TKey key)
where TKey : notnull
{
++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
/// <param name="value">增加的值</param>
public static void Increase<TKey>(this Dictionary<TKey, int> dict, TKey key, int value)
where TKey : notnull
{
// ref the value, so that we can manipulate it outside the dict.
ref int current = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
current += value;
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
/// <returns>是否存在键值</returns>
public static bool TryIncrease<TKey>(this Dictionary<TKey, int> dict, TKey key)
where TKey : notnull
{
ref int value = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
if (!Unsafe.IsNullRef(ref value))
{
++value;
return true;
}
return false;
}
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="List{T}"/> 部分
/// </summary>
public static partial class EnumerableExtension
{
/// <inheritdoc cref="Enumerable.Average(IEnumerable{int})"/>
public static double AverageNoThrow(this List<int> source)
{
Span<int> span = CollectionsMarshal.AsSpan(source);
if (span.IsEmpty)
{
return 0;
}
long sum = 0;
for (int i = 0; i < span.Length; i++)
{
sum += span[i];
}
return (double)sum / span.Length;
}
/// <summary>
/// 如果传入列表不为空则原路返回,
/// 如果传入列表为空返回一个空的列表
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <returns>源列表或空列表</returns>
public static List<TSource> EmptyIfNull<TSource>(this List<TSource>? source)
{
return source ?? new();
}
/// <summary>
/// 移除表中首个满足条件的项
/// </summary>
/// <typeparam name="T">项的类型</typeparam>
/// <param name="list">表</param>
/// <param name="shouldRemovePredicate">是否应当移除</param>
/// <returns>是否移除了元素</returns>
public static bool RemoveFirstWhere<T>(this IList<T> list, Func<T, bool> shouldRemovePredicate)
{
for (int i = 0; i < list.Count; i++)
{
if (shouldRemovePredicate.Invoke(list[i]))
{
list.RemoveAt(i);
return true;
}
}
return false;
}
}

View File

@@ -1,9 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Extension;
/// <summary>
@@ -11,26 +8,6 @@ namespace Snap.Hutao.Extension;
/// </summary>
public static partial class EnumerableExtension
{
/// <inheritdoc cref="Enumerable.Average(IEnumerable{int})"/>
public static double AverageNoThrow(this List<int> source)
{
Span<int> span = CollectionsMarshal.AsSpan(source);
if (span.IsEmpty)
{
return 0;
}
long sum = 0;
for (int i = 0; i < span.Length; i++)
{
sum += span[i];
}
return (double)sum / span.Length;
}
/// <summary>
/// 计数
/// </summary>
@@ -63,18 +40,6 @@ public static partial class EnumerableExtension
return source ?? Enumerable.Empty<TSource>();
}
/// <summary>
/// 如果传入列表不为空则原路返回,
/// 如果传入列表为空返回一个空的列表
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <returns>源列表或空列表</returns>
public static List<TSource> EmptyIfNull<TSource>(this List<TSource>? source)
{
return source ?? new();
}
/// <summary>
/// 将源转换为仅包含单个元素的枚举
/// </summary>
@@ -99,94 +64,6 @@ public static partial class EnumerableExtension
return source.FirstOrDefault(predicate) ?? source.FirstOrDefault();
}
/// <summary>
/// 获取值或默认值
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <typeparam name="TValue">值类型</typeparam>
/// <param name="dictionary">字典</param>
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>结果值</returns>
public static TValue? GetValueOrDefault2<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue? defaultValue = default)
where TKey : notnull
{
if (dictionary.TryGetValue(key, out TValue? value))
{
return value;
}
return defaultValue;
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
public static void Increase<TKey>(this Dictionary<TKey, int> dict, TKey key)
where TKey : notnull
{
++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
/// <param name="value">增加的值</param>
public static void Increase<TKey>(this Dictionary<TKey, int> dict, TKey key, int value)
where TKey : notnull
{
// ref the value, so that we can manipulate it outside the dict.
ref int current = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
current += value;
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
/// <returns>是否存在键值</returns>
public static bool TryIncrease<TKey>(this Dictionary<TKey, int> dict, TKey key)
where TKey : notnull
{
ref int value = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
if (!Unsafe.IsNullRef(ref value))
{
++value;
return true;
}
return false;
}
/// <summary>
/// 移除表中首个满足条件的项
/// </summary>
/// <typeparam name="T">项的类型</typeparam>
/// <param name="list">表</param>
/// <param name="shouldRemovePredicate">是否应当移除</param>
/// <returns>是否移除了元素</returns>
public static bool RemoveFirstWhere<T>(this IList<T> list, Func<T, bool> shouldRemovePredicate)
{
for (int i = 0; i < list.Count; i++)
{
if (shouldRemovePredicate.Invoke(list[i]))
{
list.RemoveAt(i);
return true;
}
}
return false;
}
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>
public static Dictionary<TKey, TSource> ToDictionaryOverride<TKey, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
where TKey : notnull

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text;
namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="StringBuilder"/> 扩展方法
/// </summary>
public static class StringBuilderExtensions
{
/// <summary>
/// 当条件符合时执行 <see cref="StringBuilder.Append(string?)"/>
/// </summary>
/// <param name="sb">字符串建造器</param>
/// <param name="condition">条件</param>
/// <param name="value">附加的字符串</param>
/// <returns>同一个字符串建造器</returns>
public static StringBuilder AppendIf(this StringBuilder sb, bool condition, string? value)
{
return condition ? sb.Append(value) : sb;
}
}

View File

@@ -35,6 +35,7 @@ public sealed partial class LaunchGameWindow : Window, IDisposable, IExtendedWin
scope = scopeFactory.CreateScope();
RootGrid.DataContext = scope.ServiceProvider.GetRequiredService<LaunchGameViewModel>();
Closed += (s, e) => Dispose();
}
/// <inheritdoc/>

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Windowing;
namespace Snap.Hutao.Message;
/// <summary>
/// 背景类型改变消息
/// </summary>
internal class BackdropTypeChangedMessage
{
/// <summary>
/// 构造一个新的背景类型改变消息
/// </summary>
/// <param name="backdropType">背景类型</param>
public BackdropTypeChangedMessage(BackdropType backdropType)
{
BackdropType = backdropType;
}
/// <summary>
/// 背景类型
/// </summary>
public BackdropType BackdropType { get; set; }
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Binding;
using Snap.Hutao.Model.Binding.User;
namespace Snap.Hutao.Message;

View File

@@ -0,0 +1,282 @@
// <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("20221108081525_DailyNoteEntry")]
partial class DailyNoteEntry
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
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.AvatarInfo", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Info")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DailyNote")
.HasColumnType("TEXT");
b.Property<bool>("DailyTaskNotify")
.HasColumnType("INTEGER");
b.Property<bool>("DailyTaskNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotify")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("HomeCoinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("HomeCoinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("ResinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("ResinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("ShowInHomeWidget")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotify")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("UserId");
b.ToTable("daily_notes");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("gacha_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("GachaType")
.HasColumnType("INTEGER");
b.Property<long>("Id")
.HasColumnType("INTEGER");
b.Property<int>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("QueryType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("gacha_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachUid")
.HasColumnType("TEXT");
b.Property<string>("MihoyoSDK")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("game_accounts");
});
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");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,55 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
public partial class DailyNoteEntry : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "daily_notes",
columns: table => new
{
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
Uid = table.Column<string>(type: "TEXT", nullable: false),
DailyNote = table.Column<string>(type: "TEXT", nullable: true),
ResinNotifyThreshold = table.Column<int>(type: "INTEGER", nullable: false),
ResinNotifySuppressed = table.Column<bool>(type: "INTEGER", nullable: false),
HomeCoinNotifyThreshold = table.Column<int>(type: "INTEGER", nullable: false),
HomeCoinNotifySuppressed = table.Column<bool>(type: "INTEGER", nullable: false),
TransformerNotify = table.Column<bool>(type: "INTEGER", nullable: false),
TransformerNotifySuppressed = table.Column<bool>(type: "INTEGER", nullable: false),
DailyTaskNotify = table.Column<bool>(type: "INTEGER", nullable: false),
DailyTaskNotifySuppressed = table.Column<bool>(type: "INTEGER", nullable: false),
ExpeditionNotify = table.Column<bool>(type: "INTEGER", nullable: false),
ExpeditionNotifySuppressed = table.Column<bool>(type: "INTEGER", nullable: false),
ShowInHomeWidget = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_daily_notes", x => x.InnerId);
table.ForeignKey(
name: "FK_daily_notes_users_UserId",
column: x => x.UserId,
principalTable: "users",
principalColumn: "InnerId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_daily_notes_UserId",
table: "daily_notes",
column: "UserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "daily_notes");
}
}
}

View File

@@ -82,6 +82,62 @@ namespace Snap.Hutao.Migrations
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DailyNote")
.HasColumnType("TEXT");
b.Property<bool>("DailyTaskNotify")
.HasColumnType("INTEGER");
b.Property<bool>("DailyTaskNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotify")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("HomeCoinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("HomeCoinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("ResinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("ResinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("ShowInHomeWidget")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotify")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("UserId");
b.ToTable("daily_notes");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
{
b.Property<Guid>("InnerId")
@@ -197,6 +253,17 @@ namespace Snap.Hutao.Migrations
b.Navigation("Archive");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")

View File

@@ -18,11 +18,13 @@ public class LaunchScheme
/// <param name="channel">通道</param>
/// <param name="cps">通道描述字符串</param>
/// <param name="subChannel">子通道</param>
public LaunchScheme(string name, string channel, string subChannel)
/// <param name="launcherId">启动器Id</param>
public LaunchScheme(string name, string channel, string subChannel, string launcherId)
{
Name = name;
Channel = channel;
SubChannel = subChannel;
LauncherId = launcherId;
}
/// <summary>
@@ -39,4 +41,9 @@ public class LaunchScheme
/// 子通道
/// </summary>
public string SubChannel { get; set; }
/// <summary>
/// 启动器Id
/// </summary>
public string LauncherId { get; set; }
}

View File

@@ -8,7 +8,7 @@ using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using EntityUser = Snap.Hutao.Model.Entity.User;
namespace Snap.Hutao.Model.Binding;
namespace Snap.Hutao.Model.Binding.User;
/// <summary>
/// 用于视图绑定的用户

View File

@@ -0,0 +1,34 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using EntityUser = Snap.Hutao.Model.Entity.User;
namespace Snap.Hutao.Model.Binding.User;
/// <summary>
/// 角色与实体用户
/// </summary>
public class UserAndRole
{
/// <summary>
/// 构造一个新的实体用户与角色
/// </summary>
/// <param name="user">实体用户</param>
/// <param name="role">角色</param>
public UserAndRole(EntityUser user, UserGameRole role)
{
User = user;
Role = role;
}
/// <summary>
/// 实体用户
/// </summary>
public EntityUser User { get; private set; }
/// <summary>
/// 角色
/// </summary>
public UserGameRole Role { get; private set; }
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Snap.Hutao.Model.Entity.Configuration;
/// <summary>
/// 实时便笺入口配置
/// </summary>
internal class DailyNoteEntryConfiguration : IEntityTypeConfiguration<DailyNoteEntry>
{
/// <inheritdoc/>
public void Configure(EntityTypeBuilder<DailyNoteEntry> builder)
{
builder.Property(e => e.DailyNote)
.HasColumnType("TEXT")
.HasConversion<JsonTextValueConverter<Web.Hoyolab.Takumi.GameRecord.DailyNote.DailyNote>>();
}
}

View File

@@ -0,0 +1,98 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Snap.Hutao.Model.Entity;
/// <summary>
/// 实时便笺入口
/// </summary>
[Table("daily_notes")]
public class DailyNoteEntry
{
/// <summary>
/// 内部Id
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; }
/// <summary>
/// 用户Id
/// </summary>
[ForeignKey(nameof(UserId))]
public Guid UserId { get; set; }
/// <summary>
/// 用户
/// </summary>
public User User { get; set; } = default!;
/// <summary>
/// Uid
/// </summary>
public string Uid { get; set; } = default!;
/// <summary>
/// Json!!! 实时便笺
/// </summary>
public DailyNote? DailyNote { get; set; }
/// <summary>
/// 树脂提醒阈值
/// </summary>
public int ResinNotifyThreshold { get; set; }
/// <summary>
/// 用于判断树脂是否继续提醒
/// </summary>
public bool ResinNotifySuppressed { get; set; }
/// <summary>
/// 洞天宝钱提醒阈值
/// </summary>
public int HomeCoinNotifyThreshold { get; set; }
/// <summary>
/// 用于判断洞天宝钱是否继续提醒
/// </summary>
public bool HomeCoinNotifySuppressed { get; set; }
/// <summary>
/// 参量质变仪提醒
/// </summary>
public bool TransformerNotify { get; set; }
/// <summary>
/// 用于判断参量质变仪是否继续提醒
/// </summary>
public bool TransformerNotifySuppressed { get; set; }
/// <summary>
/// 每日委托提醒
/// </summary>
public bool DailyTaskNotify { get; set; }
/// <summary>
/// 用于判断每日委托是否继续提醒
/// </summary>
public bool DailyTaskNotifySuppressed { get; set; }
/// <summary>
/// 探索派遣提醒
/// </summary>
public bool ExpeditionNotify { get; set; }
/// <summary>
/// 用于判断探索派遣是否继续提醒
/// </summary>
public bool ExpeditionNotifySuppressed { get; set; }
/// <summary>
/// 是否在主页显示小组件
/// </summary>
public bool ShowInHomeWidget { get; set; }
}

View File

@@ -26,6 +26,7 @@ public class GachaItem
/// <summary>
/// 存档
/// </summary>
[ForeignKey(nameof(ArchiveId))]
public GachaArchive Archive { get; set; } = default!;
/// <summary>

View File

@@ -22,6 +22,11 @@ public class SettingEntry
/// </summary>
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
/// <summary>
/// 窗口背景类型
/// </summary>
public const string SystemBackdropType = "SystemBackdropType";
/// <summary>
/// 启动游戏 全屏
/// </summary>

View File

@@ -43,11 +43,6 @@ public class Achievement
/// </summary>
public int Progress { get; set; }
/// <summary>
/// 触发器
/// </summary>
public IEnumerable<AchievementTrigger>? Triggers { get; set; }
/// <summary>
/// 图标
/// </summary>

View File

@@ -1,30 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Metadata.Achievement;
/// <summary>
/// 成就触发器
/// </summary>
public class AchievementTrigger
{
/// <summary>
/// 触发器类型
/// </summary>
public AchievementTriggerType Type { get; set; }
/// <summary>
/// Id
/// </summary>
public string Id { get; set; } = default!;
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; } = default!;
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; } = default!;
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
namespace Snap.Hutao.Service.DailyNote;
/// <summary>
/// 实时便笺服务
/// </summary>
[Injection(InjectAs.Singleton)]
internal class DailyNoteService
{
private readonly AppDbContext appDbContext;
/// <summary>
/// 构造一个新的实时便笺服务
/// </summary>
/// <param name="appDbContext">数据库上下文</param>
public DailyNoteService(AppDbContext appDbContext)
{
this.appDbContext = appDbContext;
}
public async ValueTask RefreshDailyNotesAndNotifyAsync()
{
GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService<GameRecordClient>();
foreach (Model.Entity.DailyNoteEntry entry in appDbContext.DailyNotes.Include(n => n.User))
{
entry.DailyNote = await gameRecordClient.GetDialyNoteAsync(entry.User, entry.Uid).ConfigureAwait(false);
}
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
}
private async ValueTask NotifyDailyNoteAsync()
{
}
}

View File

@@ -35,7 +35,7 @@ internal class GachaLogUrlStokenProvider : IGachaLogUrlProvider
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> GetQueryAsync()
{
Model.Binding.User? user = userService.Current;
Model.Binding.User.User? user = userService.Current;
if (user != null && user.SelectedUserGameRole != null)
{
if (user.Cookie!.ContainsSToken())

View File

@@ -168,27 +168,40 @@ internal class GameService : IGameService
elements = IniSerializer.Deserialize(readStream).ToList();
}
bool changed = false;
foreach (IniElement element in elements)
{
if (element is IniParameter parameter)
{
if (parameter.Key == "channel")
{
if (parameter.Value != scheme.Channel)
{
parameter.Value = scheme.Channel;
changed = true;
}
}
if (parameter.Key == "sub_channel")
{
if (parameter.Value != scheme.SubChannel)
{
parameter.Value = scheme.SubChannel;
changed = true;
}
}
}
}
if (changed)
{
using (FileStream writeStream = File.Create(configPath))
{
IniSerializer.Serialize(writeStream, elements);
}
}
}
/// <inheritdoc/>
public bool IsGameRunning()
@@ -340,6 +353,7 @@ internal class GameService : IGameService
if (isOk)
{
await ThreadHelper.SwitchToMainThreadAsync();
gameAccount.UpdateName(name);
// sync database

View File

@@ -44,7 +44,7 @@ internal class ManualGameLocator : IGameLocator
{
FileOpenPicker picker = pickerFactory.GetFileOpenPicker();
picker.FileTypeFilter.Add(".exe");
picker.SuggestedStartLocation = PickerLocationId.ComputerFolder;
picker.SuggestedStartLocation = PickerLocationId.Desktop;
// System.Runtime.InteropServices.COMException (0x80004005): Error HRESULT E_FAIL has been returned from a call to a COM component.
// Not sure what's going on here.

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service;

View File

@@ -1,10 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Web.Hoyolab;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.Model.Binding.User;
using BindingUser = Snap.Hutao.Model.Binding.User.User;
namespace Snap.Hutao.Service.User;
@@ -18,6 +17,12 @@ public interface IUserService
/// </summary>
BindingUser? Current { get; set; }
/// <summary>
/// 异步获取角色与用户集合
/// </summary>
/// <returns>角色与用户集合</returns>
Task<ObservableCollection<Model.Binding.User.UserAndRole>> GetRoleCollectionAsync();
/// <summary>
/// 初始化用户服务及所有用户
/// 异步获取同步的用户信息集合

View File

@@ -2,7 +2,7 @@
// Licensed under the MIT license.
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.Model.Binding.User;
using BindingUser = Snap.Hutao.Model.Binding.User.User;
namespace Snap.Hutao.Service.User;

View File

@@ -5,13 +5,12 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.Model.Binding.User;
using BindingUser = Snap.Hutao.Model.Binding.User.User;
namespace Snap.Hutao.Service.User;
@@ -27,6 +26,7 @@ internal class UserService : IUserService
private BindingUser? currentUser;
private ObservableCollection<BindingUser>? userCollection;
private ObservableCollection<Model.Binding.User.UserAndRole>? roleCollection;
/// <summary>
/// 构造一个新的用户服务
@@ -86,15 +86,17 @@ internal class UserService : IUserService
public async Task RemoveUserAsync(BindingUser user)
{
await Task.Yield();
Must.NotNull(userCollection!);
// Sync cache
userCollection.Remove(user);
userCollection!.Remove(user);
roleCollection!.RemoveWhere(r => r.User.InnerId == user.Entity.InnerId);
// Sync database
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Note: cascade deleted dailynotes
appDbContext.Users.RemoveAndSave(user.Entity);
}
}
@@ -137,6 +139,27 @@ internal class UserService : IUserService
return userCollection;
}
/// <inheritdoc/>
public async Task<ObservableCollection<Model.Binding.User.UserAndRole>> GetRoleCollectionAsync()
{
if (roleCollection == null)
{
List<Model.Binding.User.UserAndRole> userAndRoles = new();
ObservableCollection<BindingUser> observableUsers = await GetUserCollectionAsync().ConfigureAwait(false);
foreach (BindingUser user in observableUsers.ToList())
{
foreach (UserGameRole role in user.UserGameRoles)
{
userAndRoles.Add(new(user.Entity, role));
}
}
roleCollection = new(userAndRoles);
}
return roleCollection;
}
/// <inheritdoc/>
public async Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie)
{
@@ -148,7 +171,7 @@ internal class UserService : IUserService
{
// 检查 login ticket 是否存在
// 若存在则尝试升级至 stoken
await TryAddMultiTokenAsync(cookie, uid).ConfigureAwait(false);
await cookie.TryAddMultiTokenAsync(uid).ConfigureAwait(false);
// 检查 uid 对应用户是否存在
if (UserHelper.TryGetUserByUid(userCollection, uid, out BindingUser? userWithSameUid))
@@ -180,32 +203,14 @@ internal class UserService : IUserService
}
else if (cookie.ContainsLTokenAndCookieToken())
{
return await TryCreateUserAndAddAsync(userCollection, cookie).ConfigureAwait(false);
return await TryCreateUserAndAddAsync(cookie).ConfigureAwait(false);
}
}
return new(UserOptionResult.Incomplete, null!);
}
private async Task TryAddMultiTokenAsync(Cookie cookie, string uid)
{
if (cookie.TryGetLoginTicket(out string? loginTicket))
{
// get multitoken
Dictionary<string, string> multiToken = await Ioc.Default
.GetRequiredService<AuthClient>()
.GetMultiTokenByLoginTicketAsync(loginTicket, uid, default)
.ConfigureAwait(false);
if (multiToken.Count >= 2)
{
cookie.InsertMultiToken(uid, multiToken);
cookie.RemoveLoginTicket();
}
}
}
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(ObservableCollection<BindingUser> users, Cookie cookie)
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(Cookie cookie)
{
using (IServiceScope scope = scopeFactory.CreateScope())
{
@@ -217,8 +222,19 @@ internal class UserService : IUserService
if (newUser != null)
{
// Sync cache
if (userCollection != null)
{
await ThreadHelper.SwitchToMainThreadAsync();
users.Add(newUser);
userCollection!.Add(newUser);
if (roleCollection != null)
{
foreach (UserGameRole role in newUser.UserGameRoles)
{
roleCollection.Add(new(newUser.Entity, role));
}
}
}
// Sync database
appDbContext.Users.AddAndSave(newUser.Entity);

View File

@@ -30,6 +30,7 @@
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<DebugSymbols>true</DebugSymbols>
<DebugType>embedded</DebugType>
<ApplicationIcon>Assets\Logo.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
@@ -115,28 +116,31 @@
<PackageReference Include="CommunityToolkit.WinUI.Notifications" 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="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0" />
<!-- The PrivateAssets & IncludeAssets of Microsoft.EntityFrameworkCore.Tools should be remove to prevent multiple deps files-->
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.10" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.3.44">
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.3.48">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.0.64" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.63-beta">
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.104-beta">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.25211-preview" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.25231-preview" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.220930.4-preview2" />
<PackageReference Include="MiniExcel" Version="1.28.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="TaskScheduler" Version="2.10.1" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>

View File

@@ -148,7 +148,7 @@
BorderThickness="{ThemeResource CardBorderThickness}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
Padding="8"
Margin="0,16,0,0"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
MinHeight="48">

View File

@@ -23,21 +23,8 @@
</mxi:Interaction.Behaviors>
<shc:ScopedPage.Resources>
<shc:BindingProxy x:Key="BindingProxy" DataContext="{Binding}"/>
</shc:ScopedPage.Resources>
<Grid>
<ScrollViewer Padding="0,0,4,0">
<ItemsControl
HorizontalAlignment="Stretch"
ItemsSource="{Binding Announcement.List}"
Padding="0"
Margin="16,16,0,-6">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock
Text="{Binding TypeLabel}"
Margin="0,0,0,12"
Style="{StaticResource TitleTextBlockStyle}"/>
<DataTemplate x:Key="AnnouncementTemplate">
<cwucont:AdaptiveGridView
cwua:ItemsReorderAnimation.Duration="0:0:0.1"
SelectionMode="None"
@@ -45,7 +32,7 @@
HorizontalAlignment="Stretch"
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}"
ItemsSource="{Binding List}"
Margin="0,0,2,0">
Margin="16,16,0,-4">
<cwucont:AdaptiveGridView.ItemTemplate>
<DataTemplate>
<Border
@@ -158,10 +145,22 @@
</DataTemplate>
</cwucont:AdaptiveGridView.ItemTemplate>
</cwucont:AdaptiveGridView>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</shc:ScopedPage.Resources>
<Grid>
<ScrollViewer Padding="0,0,4,0">
<StackPanel>
<Pivot>
<PivotItem
Header="活动公告"
Content="{Binding Announcement.List[0]}"
ContentTemplate="{StaticResource AnnouncementTemplate}"/>
<PivotItem
Header="游戏公告"
Content="{Binding Announcement.List[1]}"
ContentTemplate="{StaticResource AnnouncementTemplate}"/>
</Pivot>
</StackPanel>
</ScrollViewer>
</Grid>
</shc:ScopedPage>

View File

@@ -4,13 +4,26 @@
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:shc="using:Snap.Hutao.Control"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:sc="using:SettingsUI.Controls"
xmlns:shc="using:Snap.Hutao.Control"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shv="using:Snap.Hutao.ViewModel"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance shv:DailyNoteViewModel}"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources>
<shc:BindingProxy
x:Key="ViewModelBindingProxy"
DataContext="{Binding}"/>
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
@@ -20,11 +33,62 @@
<CommandBar
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
DefaultLabelPosition="Right">
<AppBarButton Label="添加角色" Icon="{shcm:FontIcon Glyph=&#xE710;}"/>
<AppBarButton Label="立即刷新" Icon="{shcm:FontIcon Glyph=&#xE72C;}"/>
<AppBarButton Label="添加角色" Icon="{shcm:FontIcon Glyph=&#xE710;}">
<AppBarButton.Flyout>
<Flyout Placement="Bottom">
<Flyout.FlyoutPresenterStyle>
<Style
TargetType="FlyoutPresenter"
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}">
<Setter Property="Padding" Value="0,2,0,2"/>
<Setter Property="Background" Value="{ThemeResource FlyoutPresenterBackground}" />
</Style>
</Flyout.FlyoutPresenterStyle>
<StackPanel>
<TextBlock
Margin="16,12,16,16"
Text="添加角色以定时刷新"
Style="{StaticResource BaseTextBlockStyle}"/>
<ScrollViewer MaxHeight="320" Padding="16,0">
<ItemsControl ItemsSource="{Binding UserAndRoles}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Padding="0,0,0,16">
<StackPanel>
<TextBlock Text="{Binding Role.Nickname}"/>
<TextBlock
Margin="0,2,0,0"
Opacity="0.6"
Text="{Binding Role.Description}"
Style="{StaticResource CaptionTextBlockStyle}"/>
</StackPanel>
<Button
HorizontalAlignment="Right"
Content="&#xE710;"
FontFamily="{StaticResource SymbolThemeFontFamily}"
VerticalAlignment="Center"
Background="Transparent"
BorderThickness="0"
BorderBrush="{x:Null}"
Margin="16,0,0,0"
Padding="12"
Command="{Binding DataContext.TrackRoleCommand,Source={StaticResource ViewModelBindingProxy}}"
CommandParameter="{Binding}"
ToolTipService.ToolTip="添加"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</StackPanel>
</Flyout>
</AppBarButton.Flyout>
</AppBarButton>
<AppBarButton Label="通知设置" Icon="{shcm:FontIcon Glyph=&#xE713;}">
<AppBarButton.Flyout>
<Flyout>
<Flyout Placement="BottomEdgeAlignedRight">
<StackPanel>
<RadioButtons ItemsSource="{Binding RefreshTimes}">
<RadioButtons.Header>

View File

@@ -23,7 +23,7 @@
<Rectangle
Height="48"
VerticalAlignment="Top"
Fill="{StaticResource CardBackgroundFillColorSecondary}"/>
Fill="{StaticResource CardBackgroundFillColorDefaultBrush}"/>
<Pivot Grid.RowSpan="2">
<Pivot.LeftHeader>
<ComboBox

View File

@@ -23,10 +23,16 @@
<shc:BindingProxy x:Key="BindingProxy" DataContext="{Binding}"/>
<Style TargetType="Button" BasedOn="{StaticResource SettingButtonStyle}">
<Setter Property="MinWidth" Value="160"/>
<Setter Property="MinWidth" Value="156"/>
</Style>
<Style TargetType="HyperlinkButton" BasedOn="{StaticResource HyperlinkButtonStyle}">
<Setter Property="MinWidth" Value="160"/>
<Setter Property="MinWidth" Value="156"/>
</Style>
<Style TargetType="ComboBox" BasedOn="{StaticResource DefaultComboBoxStyle}">
<Setter Property="MinWidth" Value="156"/>
</Style>
<Style TargetType="NumberBox">
<Setter Property="MinWidth" Value="158"/>
</Style>
</Page.Resources>
<Grid>
@@ -48,7 +54,6 @@
Description="切换游戏服务器B服用户需要自备额外的 PCGameSDK.dll 文件">
<sc:Setting.ActionContent>
<ComboBox
Width="160"
ItemsSource="{Binding KnownSchemes}"
SelectedItem="{Binding SelectedScheme,Mode=TwoWay}"
DisplayMemberPath="Name"/>
@@ -56,30 +61,20 @@
</sc:Setting>
<sc:SettingExpander IsExpanded="True">
<sc:SettingExpander.Header>
<Grid Padding="0,16">
<StackPanel Orientation="Horizontal">
<FontIcon Glyph="&#xE748;"/>
<StackPanel VerticalAlignment="Center">
<TextBlock
Margin="20,0,0,0"
Text="账号"/>
<TextBlock
Opacity="0.8"
Margin="20,0,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="在游戏内切换账号,网络环境发生变化后需要重新手动检测"/>
</StackPanel>
</StackPanel>
<sc:Setting
Icon="&#xE748;"
Header="账号"
Description="在游戏内切换账号,网络环境发生变化后需要重新手动检测">
<sc:Setting.ActionContent>
<Button
HorizontalAlignment="Right"
Command="{Binding DetectGameAccountCommand}"
Grid.Column="1"
Margin="0,0,8,0"
Width="128"
MinWidth="128"
MinWidth="124"
Content="检测"/>
</Grid>
</sc:Setting.ActionContent>
</sc:Setting>
</sc:SettingExpander.Header>
<ListView
ItemsSource="{Binding GameAccounts}"
@@ -118,7 +113,7 @@
CommandParameter="{Binding}"
FontFamily="{StaticResource SymbolThemeFontFamily}"/>
<Button
Margin="4,8"
Margin="4,8,0,8"
MinWidth="48"
VerticalAlignment="Stretch"
ToolTipService.ToolTip="删除"

View File

@@ -16,6 +16,9 @@
<Style TargetType="HyperlinkButton" BasedOn="{StaticResource HyperlinkButtonStyle}">
<Setter Property="MinWidth" Value="160"/>
</Style>
<Style TargetType="ComboBox" BasedOn="{StaticResource DefaultComboBoxStyle}">
<Setter Property="MinWidth" Value="160"/>
</Style>
</Page.Resources>
<ScrollViewer>
<Grid>
@@ -90,6 +93,19 @@
</sc:SettingExpander>
</sc:SettingsGroup>
<sc:SettingsGroup Header="外观">
<sc:Setting
Icon="&#xE7F7;"
Header="背景材质"
Description="更改窗体的背景材质">
<ComboBox
SelectedItem="{Binding SelectedBackdropType,Mode=TwoWay}"
ItemsSource="{Binding BackdropTypes}"
DisplayMemberPath="Name"/>
</sc:Setting>
</sc:SettingsGroup>
<sc:SettingsGroup Header="祈愿记录">
<sc:Setting
Icon="&#xE81C;"

View File

@@ -198,7 +198,6 @@
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>

View File

@@ -6,6 +6,7 @@ using Snap.Hutao.Control;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.AvatarInfo;
using Snap.Hutao.Service.User;
@@ -79,7 +80,7 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
private Task OpenUIAsync()
{
if (userService.Current is Model.Binding.User user)
if (userService.Current is User user)
{
if (user.SelectedUserGameRole is UserGameRole role)
{
@@ -92,7 +93,7 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
private Task RefreshByUserGameRoleAsync()
{
if (userService.Current is Model.Binding.User user)
if (userService.Current is User user)
{
if (user.SelectedUserGameRole is UserGameRole role)
{

View File

@@ -3,7 +3,11 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Control;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Service.User;
using System.Collections.ObjectModel;
namespace Snap.Hutao.ViewModel;
@@ -11,8 +15,10 @@ namespace Snap.Hutao.ViewModel;
/// 实时便笺视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
internal class DailyNoteViewModel : ObservableObject, ISupportCancellation, IDisposable
{
private readonly IUserService userService;
private readonly List<NamedValue<int>> refreshTimes = new()
{
new("4 分钟 | 0.5 树脂", 240),
@@ -22,6 +28,23 @@ internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
new("60 分钟 | 7.5 树脂", 3600),
};
private bool isReminderNotification;
private NamedValue<int>? selectedRefreshTime;
private ObservableCollection<UserAndRole>? userAndRoles;
/// <summary>
/// 构造一个新的实时便笺视图模型
/// </summary>
/// <param name="userService">用户服务</param>
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
public DailyNoteViewModel(IUserService userService, IAsyncRelayCommandFactory asyncRelayCommandFactory)
{
this.userService = userService;
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
TrackRoleCommand = asyncRelayCommandFactory.Create(TrackRoleAsync);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
@@ -29,4 +52,44 @@ internal class DailyNoteViewModel : ObservableObject, ISupportCancellation
/// 刷新时间
/// </summary>
public List<NamedValue<int>> RefreshTimes { get => refreshTimes; }
/// <summary>
/// 选中的刷新时间
/// </summary>
public NamedValue<int>? SelectedRefreshTime { get => selectedRefreshTime; set => SetProperty(ref selectedRefreshTime, value); }
/// <summary>
/// 提醒式通知
/// </summary>
public bool IsReminderNotification { get => isReminderNotification; set => SetProperty(ref isReminderNotification, value); }
/// <summary>
/// 用户与角色集合
/// </summary>
public ObservableCollection<UserAndRole>? UserAndRoles { get => userAndRoles; set => userAndRoles = value; }
/// <summary>
/// 打开界面命令
/// </summary>
public ICommand OpenUICommand { get; }
/// <summary>
/// 跟踪角色命令
/// </summary>
public ICommand TrackRoleCommand { get; }
public void Dispose()
{
throw new NotImplementedException();
}
private async Task OpenUIAsync()
{
UserAndRoles = await userService.GetRoleCollectionAsync().ConfigureAwait(true);
}
private async Task TrackRoleAsync()
{
}
}

View File

@@ -68,7 +68,7 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
IUserService userService = Ioc.Default.GetRequiredService<IUserService>();
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
if (userService.Current is Model.Binding.User user)
if (userService.Current is Model.Binding.User.User user)
{
if (user.SelectedUserGameRole == null)
{

View File

@@ -35,8 +35,8 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
private readonly List<LaunchScheme> knownSchemes = new()
{
new LaunchScheme(name: "官方服 | 天空岛", channel: "1", subChannel: "1"),
new LaunchScheme(name: "渠道服 | 世界树", channel: "14", subChannel: "0"),
new LaunchScheme(name: "官方服 | 天空岛", channel: "1", subChannel: "1", launcherId: "18"),
new LaunchScheme(name: "渠道服 | 世界树", channel: "14", subChannel: "0", launcherId: "17"),
// new LaunchScheme(name: "国际服 | 暂不支持", channel: "1", subChannel: "0"),
};
@@ -231,7 +231,7 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
}
catch (UnauthorizedAccessException)
{
infoBarService.Warning("切换服务器失败,保存配置文件时发生异常\n请以管理员模式启动胡桃。");
infoBarService.Warning("读取或保存配置文件时发生异常请以管理员模式启动胡桃。");
}
}
@@ -239,7 +239,7 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
{
if (!gameService.SetGameAccount(SelectedGameAccount))
{
Ioc.Default.GetRequiredService<IInfoBarService>().Warning("切换账号失败");
infoBarService.Warning("切换账号失败");
}
}

View File

@@ -3,10 +3,12 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Locator;
@@ -22,9 +24,17 @@ internal class SettingViewModel : ObservableObject
private readonly AppDbContext appDbContext;
private readonly IGameService gameService;
private readonly SettingEntry isEmptyHistoryWishVisibleEntry;
private readonly SettingEntry selectedBackdropTypeEntry;
private readonly List<NamedValue<BackdropType>> backdropTypes = new()
{
new("亚克力", BackdropType.Acrylic),
new("云母", BackdropType.Mica),
new("变种云母", BackdropType.MicaAlt),
};
private bool isEmptyHistoryWishVisible;
private string gamePath;
private NamedValue<BackdropType> selectedBackdropType;
/// <summary>
/// 构造一个新的测试视图模型
@@ -40,10 +50,16 @@ internal class SettingViewModel : ObservableObject
Experimental = experimental;
isEmptyHistoryWishVisibleEntry = appDbContext.Settings
.SingleOrAdd(e => e.Key == SettingEntry.IsEmptyHistoryWishVisible, () => new(SettingEntry.IsEmptyHistoryWishVisible, true.ToString()), out _);
isEmptyHistoryWishVisibleEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.IsEmptyHistoryWishVisible, true.ToString());
IsEmptyHistoryWishVisible = bool.Parse(isEmptyHistoryWishVisibleEntry.Value!);
selectedBackdropTypeEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.SystemBackdropType, BackdropType.Mica.ToString());
BackdropType type = Enum.Parse<BackdropType>(selectedBackdropTypeEntry.Value!);
// prevent unnecessary backdrop setting.
selectedBackdropType = backdropTypes.Single(t => t.Value == type);
OnPropertyChanged(nameof(SelectedBackdropType));
GamePath = gameService.GetGamePathSkipLocator();
SetGamePathCommand = asyncRelayCommandFactory.Create(SetGamePathAsync);
@@ -67,11 +83,13 @@ internal class SettingViewModel : ObservableObject
get => isEmptyHistoryWishVisible;
set
{
SetProperty(ref isEmptyHistoryWishVisible, value);
if (SetProperty(ref isEmptyHistoryWishVisible, value))
{
isEmptyHistoryWishVisibleEntry.Value = value.ToString();
appDbContext.Settings.UpdateAndSave(isEmptyHistoryWishVisibleEntry);
}
}
}
/// <summary>
/// 游戏路径
@@ -83,6 +101,29 @@ internal class SettingViewModel : ObservableObject
set => SetProperty(ref gamePath, value);
}
/// <summary>
/// 背景类型
/// </summary>
public List<NamedValue<BackdropType>> BackdropTypes { get => backdropTypes; }
/// <summary>
/// 选中的背景类型
/// </summary>
public NamedValue<BackdropType> SelectedBackdropType
{
get => selectedBackdropType;
[MemberNotNull(nameof(selectedBackdropType))]
set
{
if (SetProperty(ref selectedBackdropType, value))
{
selectedBackdropTypeEntry.Value = value.Value.ToString();
appDbContext.Settings.UpdateAndSave(selectedBackdropTypeEntry);
Ioc.Default.GetRequiredService<IMessenger>().Send(new Message.BackdropTypeChangedMessage(value.Value));
}
}
}
/// <summary>
/// 实验性功能
/// </summary>

View File

@@ -6,7 +6,7 @@ using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Core.IO.DataTransfer;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.User;

View File

@@ -43,6 +43,17 @@ internal static class ApiEndpoints
/// </summary>
public const string GameRecordCharacter = $"{ApiTakumiRecordApi}/character";
/// <summary>
/// 游戏记录实时便笺
/// </summary>
/// <param name="uid">uid</param>
/// <param name="server">服务器区域</param>
/// <returns>游戏记录实时便笺字符串</returns>
public static string GameRecordDailyNote(string uid, string server)
{
return $"{ApiTakumiRecordApi}/dailyNote?server={server}&role_id={uid}";
}
/// <summary>
/// 游戏记录主页
/// </summary>
@@ -121,12 +132,13 @@ internal static class ApiEndpoints
/// <summary>
/// 启动器资源
/// </summary>
/// <param name="launcherId">启动器Id</param>
/// <param name="channel">通道</param>
/// <param name="subChannel">子通道</param>
/// <returns>启动器资源字符串</returns>
public static string SdkStaticLauncherResource(string channel, string subChannel)
public static string SdkStaticLauncherResource(string launcherId, string channel, string subChannel)
{
return $"{SdkStaticLauncherApi}/resource?key=eYd89JmJ&launcher_id=18&channel_id={channel}&sub_channel_id={subChannel}";
return $"{SdkStaticLauncherApi}/resource?key=eYd89JmJ&launcher_id={launcherId}&channel_id={channel}&sub_channel_id={subChannel}";
}
// https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api/content?filter_adv=true&key=eYd89JmJ&language=zh-cn&launcher_id=18

View File

@@ -36,7 +36,7 @@ internal class UserClient
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>详细信息</returns>
public async Task<UserInfo?> GetUserFullInfoAsync(Model.Binding.User user, CancellationToken token = default)
public async Task<UserInfo?> GetUserFullInfoAsync(Model.Binding.User.User user, CancellationToken token = default)
{
Response<UserFullInfoWrapper>? resp = await httpClient
.SetUser(user)

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab;
/// <summary>
/// 键部分
/// </summary>
[SuppressMessage("", "SA1310")]
[SuppressMessage("", "SA1600")]
public partial class Cookie
{
public const string COOKIE_TOKEN = "cookie_token";
public const string ACCOUNT_ID = "account_id";
public const string LOGIN_TICKET = "login_ticket";
public const string LOGIN_UID = "login_uid";
public const string LTOKEN = "ltoken";
public const string LTUID = "ltuid";
public const string STOKEN = "stoken";
public const string STUID = "stuid";
}

View File

@@ -3,6 +3,7 @@
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
namespace Snap.Hutao.Web.Hoyolab;
@@ -90,20 +91,6 @@ public partial class Cookie
return inner.ContainsKey(STOKEN);
}
/// <summary>
/// 插入Stoken
/// </summary>
/// <param name="uid">uid</param>
/// <param name="multiToken">tokens</param>
public void InsertMultiToken(string uid, Dictionary<string, string> multiToken)
{
inner[STUID] = uid;
inner[STOKEN] = multiToken[STOKEN];
inner[LTUID] = uid;
inner[LTOKEN] = multiToken[LTOKEN];
}
/// <summary>
/// 插入 Stoken
/// </summary>
@@ -115,15 +102,6 @@ public partial class Cookie
inner[STOKEN] = cookie.inner[STOKEN];
}
/// <summary>
/// 移除 LoginTicket
/// </summary>
public void RemoveLoginTicket()
{
inner.Remove(LOGIN_TICKET);
inner.Remove(LOGIN_UID);
}
/// <summary>
/// 移除无效的键
/// </summary>
@@ -186,6 +164,34 @@ public partial class Cookie
}
}
/// <summary>
/// 异步尝试添加MultiToken
/// </summary>
/// <param name="uid">uid</param>
/// <returns>任务</returns>
public async Task TryAddMultiTokenAsync(string uid)
{
if (TryGetLoginTicket(out string? loginTicket))
{
// get multitoken
Dictionary<string, string> multiToken = await Ioc.Default
.GetRequiredService<AuthClient>()
.GetMultiTokenByLoginTicketAsync(loginTicket, uid, default)
.ConfigureAwait(false);
if (multiToken.Count >= 2)
{
inner[STUID] = uid;
inner[STOKEN] = multiToken[STOKEN];
inner[LTUID] = uid;
inner[LTOKEN] = multiToken[LTOKEN];
inner.Remove(LOGIN_TICKET);
inner.Remove(LOGIN_UID);
}
}
}
/// <summary>
/// 转换为Cookie的字符串表示
/// </summary>
@@ -195,23 +201,3 @@ public partial class Cookie
return string.Join(';', inner.Select(kvp => $"{kvp.Key}={kvp.Value}"));
}
}
/// <summary>
/// 键部分
/// </summary>
[SuppressMessage("", "SA1310")]
[SuppressMessage("", "SA1600")]
public partial class Cookie
{
public const string COOKIE_TOKEN = "cookie_token";
public const string ACCOUNT_ID = "account_id";
public const string LOGIN_TICKET = "login_ticket";
public const string LOGIN_UID = "login_uid";
public const string LTOKEN = "ltoken";
public const string LTUID = "ltuid";
public const string STOKEN = "stoken";
public const string STUID = "stuid";
}

View File

@@ -2,7 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Model.Binding;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Web.Request;
using System.Net.Http;
using System.Net.Http.Json;
@@ -64,7 +64,7 @@ internal static class HttpClientExtensions
/// 设置用户的Cookie
/// </summary>
/// <param name="httpClient">http客户端</param>
/// <param name="user">用户</param>
/// <param name="user">绑定用户</param>
/// <returns>客户端</returns>
internal static HttpClient SetUser(this HttpClient httpClient, User user)
{
@@ -72,6 +72,18 @@ internal static class HttpClientExtensions
return httpClient;
}
/// <summary>
/// 设置用户的Cookie
/// </summary>
/// <param name="httpClient">http客户端</param>
/// <param name="user">实体用户</param>
/// <returns>客户端</returns>
internal static HttpClient SetUser(this HttpClient httpClient, Model.Entity.User user)
{
httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie!.ToString());
return httpClient;
}
/// <summary>
/// 设置Referer
/// </summary>

View File

@@ -41,6 +41,11 @@ public struct PlayerUid
get => region ??= EvaluateRegion(Value[0]);
}
public static implicit operator PlayerUid(string source)
{
return new(source);
}
/// <inheritdoc/>
public override string ToString()
{

View File

@@ -39,8 +39,9 @@ internal class ResourceClient
/// <returns>游戏资源</returns>
public async Task<GameResource?> GetResourceAsync(LaunchScheme scheme, CancellationToken token = default)
{
string url = ApiEndpoints.SdkStaticLauncherResource(scheme.LauncherId, scheme.Channel, scheme.SubChannel);
Response<GameResource>? response = await httpClient
.TryCatchGetFromJsonAsync<Response<GameResource>>(ApiEndpoints.SdkStaticLauncherResource(scheme.Channel, scheme.SubChannel), options, logger, token)
.TryCatchGetFromJsonAsync<Response<GameResource>>(url, options, logger, token)
.ConfigureAwait(false);
return response?.Data;

View File

@@ -3,7 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Web.Response;
using System.Net.Http;

View File

@@ -2,7 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Binding;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Response;
using System.Net.Http;

View File

@@ -64,7 +64,7 @@ public class UserGameRole
get => $"{RegionName} | Lv.{Level}";
}
public static explicit operator PlayerUid(UserGameRole userGameRole)
public static implicit operator PlayerUid(UserGameRole userGameRole)
{
return new PlayerUid(userGameRole.GameUid, userGameRole.Region);
}

View File

@@ -0,0 +1,171 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
/// <summary>
/// 实时便笺
/// </summary>
public class DailyNote
{
/// <summary>
/// 当前树脂
/// </summary>
[JsonPropertyName("current_resin")]
public int CurrentResin { get; set; }
/// <summary>
/// 最大树脂
/// </summary>
[JsonPropertyName("max_resin")]
public int MaxResin { get; set; }
/// <summary>
/// 树脂恢复时间 <see cref="string"/>类型的秒数
/// </summary>
[JsonPropertyName("resin_recovery_time")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public int ResinRecoveryTime { get; set; }
/// <summary>
/// 格式化的树脂恢复时间
/// </summary>
public string ResinRecoveryTargetTime
{
get
{
DateTime tt = DateTime.Now.AddSeconds(ResinRecoveryTime);
int totalDays = (tt - DateTime.Today).Days;
string day = totalDays switch
{
0 => "今天",
1 => "明天",
2 => "后天",
_ => $"{totalDays}天",
};
return $"{day} {tt:HH:mm}";
}
}
/// <summary>
/// 委托完成数
/// </summary>
[JsonPropertyName("finished_task_num")]
public int FinishedTaskNum { get; set; }
/// <summary>
/// 委托总数
/// </summary>
[JsonPropertyName("total_task_num")]
public int TotalTaskNum { get; set; }
/// <summary>
/// 4次委托额外奖励是否领取
/// </summary>
[JsonPropertyName("is_extra_task_reward_received")]
public bool IsExtraTaskRewardReceived { get; set; }
/// <summary>
/// 每日委托奖励字符串
/// </summary>
public string ExtraTaskRewardDescription
{
get
{
return IsExtraTaskRewardReceived
? "已领取「每日委托」奖励"
: FinishedTaskNum == TotalTaskNum
? "「每日委托」奖励待领取"
: "今日完成委托次数不足";
}
}
/// <summary>
/// 剩余周本折扣次数
/// </summary>
[JsonPropertyName("remain_resin_discount_num")]
public int RemainResinDiscountNum { get; set; }
/// <summary>
/// 周本树脂减免使用次数
/// </summary>
public int ResinDiscountUsedNum
{
get => ResinDiscountNumLimit - RemainResinDiscountNum;
}
/// <summary>
/// 周本折扣总次数
/// </summary>
[JsonPropertyName("resin_discount_num_limit")]
public int ResinDiscountNumLimit { get; set; }
/// <summary>
/// 当前派遣数
/// </summary>
[JsonPropertyName("current_expedition_num")]
public int CurrentExpeditionNum { get; set; }
/// <summary>
/// 最大派遣数
/// </summary>
[JsonPropertyName("max_expedition_num")]
public int MaxExpeditionNum { get; set; }
/// <summary>
/// 派遣
/// </summary>
[JsonPropertyName("expeditions")]
public List<Expedition> Expeditions { get; set; } = default!;
/// <summary>
/// 当前洞天宝钱
/// </summary>
[JsonPropertyName("current_home_coin")]
public int CurrentHomeCoin { get; set; }
/// <summary>
/// 最大洞天宝钱
/// </summary>
[JsonPropertyName("max_home_coin")]
public int MaxHomeCoin { get; set; }
/// <summary>
/// 洞天宝钱恢复时间 <see cref="string"/>类型的秒数
/// </summary>
[JsonPropertyName("home_coin_recovery_time")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public int HomeCoinRecoveryTime { get; set; }
/// <summary>
/// 格式化的洞天宝钱恢复时间
/// </summary>
public string HomeCoinRecoveryTargetTimeFormatted
{
get
{
DateTime reach = DateTime.Now.AddSeconds(HomeCoinRecoveryTime);
int totalDays = (reach - DateTime.Today).Days;
string day = totalDays switch
{
0 => "今天",
1 => "明天",
2 => "后天",
_ => $"{totalDays}天",
};
return $"{day} {reach:HH:mm}";
}
}
/// <summary>
/// 日历链接
/// </summary>
[JsonPropertyName("calendar_url")]
public string CalendarUrl { get; set; } = default!;
/// <summary>
/// 参量质变仪
/// </summary>
[JsonPropertyName("transformer")]
public Transformer Transformer { get; set; } = default!;
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
/// <summary>
/// 探索派遣
/// </summary>
public class Expedition
{
/// <summary>
/// 图标
/// </summary>
[JsonPropertyName("avatar_side_icon")]
public Uri AvatarSideIcon { get; set; } = default!;
/// <summary>
/// 状态 Ongoing:派遣中 Finished:已完成
/// </summary>
[JsonPropertyName("status")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public ExpeditionStatus Status { get; set; }
/// <summary>
/// 剩余时间
/// </summary>
[JsonPropertyName("remained_time")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public int RemainedTime { get; set; }
/// <summary>
/// 格式化的剩余时间
/// </summary>
public string RemainedTimeFormatted
{
get
{
if (Status == ExpeditionStatus.Finished)
{
return "已完成";
}
TimeSpan ts = new(0, 0, RemainedTime);
return ts.Hours > 0 ? $"{ts.Hours}时" : $"{ts.Minutes}分";
}
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
/// <summary>
/// 探索派遣状态
/// </summary>
public enum ExpeditionStatus
{
/// <summary>
/// 进行中
/// </summary>
Ongoing,
/// <summary>
/// 完成
/// </summary>
Finished,
}

View File

@@ -0,0 +1,77 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using System.Text;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
/// <summary>
/// 参量质变仪恢复时间包装
/// 已准备完成 $后可再次使用
/// 冷却中 可使用
/// </summary>
public class RecoveryTime
{
/// <summary>
/// 日
/// </summary>
[JsonPropertyName("Day")]
public int Day { get; set; }
/// <summary>
/// 时
/// </summary>
[JsonPropertyName("Hour")]
public int Hour { get; set; }
/// <summary>
/// 分
/// </summary>
[JsonPropertyName("Minute")]
public int Minute { get; set; }
/// <summary>
/// 秒
/// </summary>
[JsonPropertyName("Second")]
public int Second { get; set; }
/// <summary>
/// 是否已经到达
/// </summary>
[JsonPropertyName("reached")]
public bool Reached { get; set; }
/// <summary>
/// 获取格式化的剩余时间
/// </summary>
public string TimeFormatted
{
get
{
if (Reached)
{
return "已准备完成";
}
else
{
return new StringBuilder()
.AppendIf(Day > 0, $"{Day}天")
.AppendIf(Hour > 0, $"{Hour}时")
.AppendIf(Minute > 0, $"{Minute}分")
.AppendIf(Second > 0, $"{Second}秒")
.Append(" 后可再次使用")
.ToString();
}
}
}
/// <summary>
/// 获取格式化的状态
/// </summary>
public string ReachedFormatted
{
get => Reached ? "可使用" : "冷却中";
}
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
/// <summary>
/// 参量质变仪
/// </summary>
public class Transformer
{
/// <summary>
/// 是否拥有该道具
/// </summary>
[JsonPropertyName("obtained")]
public bool Obtained { get; set; }
/// <summary>
/// 恢复时间包装
/// </summary>
[JsonPropertyName("recovery_time")]
public RecoveryTime? RecoveryTime { get; set; }
/// <summary>
/// Wiki链接
/// </summary>
[JsonPropertyName("wiki")]
public Uri Wiki { get; set; } = default!;
/// <summary>
/// 是否提醒
/// </summary>
[JsonPropertyName("noticed")]
public bool Noticed { get; set; }
/// <summary>
/// 上个任务的Id
/// </summary>
[JsonPropertyName("latest_job_id")]
public string LastJobId { get; set; } = default!;
}

View File

@@ -3,7 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using Snap.Hutao.Web.Response;
@@ -34,6 +34,42 @@ internal class GameRecordClient
this.logger = logger;
}
/// <summary>
/// 异步获取实时便笺
/// </summary>
/// <param name="user">用户</param>
/// <param name="uid">查询uid</param>
/// <param name="token">取消令牌</param>
/// <returns>实时便笺</returns>
public async Task<DailyNote.DailyNote?> GetDialyNoteAsync(User user, PlayerUid uid, CancellationToken token = default)
{
Response<DailyNote.DailyNote>? resp = await httpClient
.SetUser(user)
.UsingDynamicSecret(options, ApiEndpoints.GameRecordDailyNote(uid.Value, uid.Region))
.GetFromJsonAsync<Response<DailyNote.DailyNote>>(token)
.ConfigureAwait(false);
return resp?.Data;
}
/// <summary>
/// 异步获取实时便笺
/// </summary>
/// <param name="user">用户</param>
/// <param name="uid">查询uid</param>
/// <param name="token">取消令牌</param>
/// <returns>实时便笺</returns>
public async Task<DailyNote.DailyNote?> GetDialyNoteAsync(Model.Entity.User user, PlayerUid uid, CancellationToken token = default)
{
Response<DailyNote.DailyNote>? resp = await httpClient
.SetUser(user)
.UsingDynamicSecret(options, ApiEndpoints.GameRecordDailyNote(uid.Value, uid.Region))
.GetFromJsonAsync<Response<DailyNote.DailyNote>>(token)
.ConfigureAwait(false);
return resp?.Data;
}
/// <summary>
/// 获取玩家基础信息
/// </summary>
@@ -68,7 +104,7 @@ internal class GameRecordClient
/// 获取玩家深渊信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="schedule">1当期2期</param>
/// <param name="schedule">期</param>
/// <param name="token">取消令牌</param>
/// <returns>深渊信息</returns>
public Task<SpiralAbyss.SpiralAbyss?> GetSpiralAbyssAsync(User user, SpiralAbyssSchedule schedule, CancellationToken token = default)

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
@@ -172,7 +173,7 @@ internal class HomaClient
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>玩家记录</returns>
public async Task<SimpleRecord> GetPlayerRecordAsync(Snap.Hutao.Model.Binding.User user, CancellationToken token = default)
public async Task<SimpleRecord> GetPlayerRecordAsync(User user, CancellationToken token = default)
{
PlayerInfo? playerInfo = await gameRecordClient
.GetPlayerInfoAsync(user, token)

View File

@@ -41,7 +41,7 @@ public enum KnownReturnCode : int
/// <summary>
/// 访问过于频繁
/// </summary>
VIsitTooFrequently = -110,
VisitTooFrequently = -110,
/// <summary>
/// 应用Id错误

View File

@@ -89,14 +89,4 @@ public class Response<TData> : Response
{
return ReturnCode == 0;
}
/// <inheritdoc/>
public override int GetHashCode()
{
int j = ReturnCode.GetHashCode();
int k = Message == null ? 0 : Message.GetHashCode();
int i = Data == null ? 0 : Data.GetHashCode();
return (((j * 31) + k) * 31) + i;
}
}