[skip ci] uigf 4 support part 4

This commit is contained in:
DismissedLight
2024-07-14 21:41:07 +08:00
parent e98bee8a9b
commit d30ef6daa0
58 changed files with 902 additions and 1500 deletions

View File

@@ -18,9 +18,8 @@ internal static class ValueFileExtension
return new(true, t);
}
}
catch (Exception ex)
catch (Exception)
{
_ = ex;
return new(false, null);
}
}

View File

@@ -15,8 +15,7 @@ namespace Snap.Hutao.Model.Entity;
internal sealed class Achievement : IAppDbEntityHasArchive,
IEquatable<Achievement>,
IDbMappingForeignKeyFrom<Achievement, AchievementId>,
IDbMappingForeignKeyFrom<Achievement, UIAFItem>,
IDbMappingForeignKeyFrom<Achievement, HutaoReservedAchievement>
IDbMappingForeignKeyFrom<Achievement, UIAFItem>
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
@@ -58,18 +57,6 @@ internal sealed class Achievement : IAppDbEntityHasArchive,
};
}
public static Achievement From(Guid archiveId, HutaoReservedAchievement achievement)
{
return new()
{
ArchiveId = archiveId,
Id = achievement.Id,
Current = achievement.Current,
Status = achievement.Status,
Time = achievement.Time,
};
}
public bool Equals(Achievement? other)
{
if (other is null)

View File

@@ -27,7 +27,7 @@ internal sealed class CultivateEntry : IAppDbEntity,
public uint Id { get; set; }
public static CultivateEntry From(in Guid projectId, in CultivateType type, in uint id)
public static CultivateEntry From(Guid projectId, CultivateType type, uint id)
{
return new()
{

View File

@@ -25,7 +25,7 @@ internal sealed class CultivateItem : IDbMappingForeignKeyFrom<CultivateItem, We
public bool IsFinished { get; set; }
public static CultivateItem From(in Guid entryId, in Web.Hoyolab.Takumi.Event.Calculate.Item item)
public static CultivateItem From(Guid entryId, Web.Hoyolab.Takumi.Event.Calculate.Item item)
{
return new()
{

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity.Abstraction;
using Snap.Hutao.Model.InterChange;
using Snap.Hutao.Model.InterChange.GachaLog;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using System.ComponentModel.DataAnnotations;
@@ -14,8 +13,6 @@ namespace Snap.Hutao.Model.Entity;
[Table("gacha_items")]
internal sealed partial class GachaItem
: IDbMappingForeignKeyFrom<GachaItem, GachaLogItem, uint>,
IDbMappingForeignKeyFrom<GachaItem, LegacyUIGFItem, uint>,
IDbMappingForeignKeyFrom<GachaItem, LegacyUIGFItem>,
IDbMappingForeignKeyFrom<GachaItem, Web.Hutao.GachaLog.GachaItem>,
IDbMappingForeignKeyFrom<GachaItem, Hk4eItem, int>
{
@@ -51,32 +48,6 @@ internal sealed partial class GachaItem
};
}
public static GachaItem From(Guid archiveId, LegacyUIGFItem item, uint itemId)
{
return new()
{
ArchiveId = archiveId,
GachaType = item.GachaType,
QueryType = item.UIGFGachaType,
ItemId = itemId,
Time = item.Time,
Id = item.Id,
};
}
public static GachaItem From(Guid archiveId, LegacyUIGFItem item)
{
return new()
{
ArchiveId = archiveId,
GachaType = item.GachaType,
QueryType = item.UIGFGachaType,
ItemId = uint.Parse(item.ItemId, CultureInfo.CurrentCulture),
Time = item.Time,
Id = item.Id,
};
}
public static GachaItem From(Guid archiveId, Web.Hutao.GachaLog.GachaItem item)
{
return new()

View File

@@ -15,41 +15,20 @@ namespace Snap.Hutao.Model.Entity;
internal sealed class InventoryItem : IDbMappingForeignKeyFrom<InventoryItem, uint>,
IDbMappingForeignKeyFrom<InventoryItem, uint, uint>
{
/// <summary>
/// 内部Id
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; }
/// <summary>
/// 培养计划Id
/// </summary>
public Guid ProjectId { get; set; }
/// <summary>
/// 所属的计划
/// </summary>
[ForeignKey(nameof(ProjectId))]
public CultivateProject Project { get; set; } = default!;
/// <summary>
/// 物品Id
/// </summary>
public uint ItemId { get; set; }
/// <summary>
/// 个数 4294967295
/// </summary>
public uint Count { get; set; }
/// <summary>
/// 构造一个新的个数为0的物品
/// </summary>
/// <param name="projectId">项目Id</param>
/// <param name="itemId">物品Id</param>
/// <returns>新的个数为0的物品</returns>
public static InventoryItem From(in Guid projectId, in uint itemId)
public static InventoryItem From(Guid projectId, uint itemId)
{
return new()
{
@@ -58,14 +37,7 @@ internal sealed class InventoryItem : IDbMappingForeignKeyFrom<InventoryItem, ui
};
}
/// <summary>
/// 构造一个新的个数不为0的物品
/// </summary>
/// <param name="projectId">项目Id</param>
/// <param name="itemId">物品Id</param>
/// <param name="count">物品个数</param>
/// <returns>新的个数不为0的物品</returns>
public static InventoryItem From(in Guid projectId, in uint itemId, in uint count)
public static InventoryItem From(Guid projectId, uint itemId, uint count)
{
return new()
{

View File

@@ -6,7 +6,7 @@ using Snap.Hutao.Core.Json.Annotation;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
namespace Snap.Hutao.Model.InterChange;
namespace Snap.Hutao.Model.InterChange.GachaLog;
internal sealed class Hk4eItem : IMappingFrom<Hk4eItem, GachaItem>
{

View File

@@ -1,111 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Web.Hoyolab;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Model.InterChange.GachaLog;
/// <summary>
/// 统一可交换祈愿格式
/// https://uigf.org/standards/UIGF.html
/// </summary>
[HighQuality]
[Obsolete]
internal sealed class LegacyUIGF : IJsonOnSerializing, IJsonOnDeserialized
{
/// <summary>
/// 当前版本
/// </summary>
public const string CurrentVersion = "v3.0";
/// <summary>
/// 信息
/// </summary>
[JsonRequired]
[JsonPropertyName("info")]
public LegacyUIGFInfo Info { get; set; } = default!;
/// <summary>
/// 列表
/// </summary>
[JsonPropertyName("list")]
public List<LegacyUIGFItem> List { get; set; } = default!;
public void OnSerializing()
{
TimeSpan offset = GetRegionTimeZoneUtcOffset();
foreach (LegacyUIGFItem item in List)
{
item.Time = item.Time.ToOffset(offset);
}
}
public void OnDeserialized()
{
// Adjust items timezone
TimeSpan offset = GetRegionTimeZoneUtcOffset();
foreach (LegacyUIGFItem item in List)
{
item.Time = UnsafeDateTimeOffset.AdjustOffsetOnly(item.Time, offset);
}
}
public bool IsCurrentVersionSupported(out LegacyUIGFVersion version)
{
version = Info.UIGFVersion switch
{
"v2.1" => LegacyUIGFVersion.Major2Minor2OrLower,
"v2.2" => LegacyUIGFVersion.Major2Minor2OrLower,
"v2.3" => LegacyUIGFVersion.Major2Minor3OrHigher,
"v2.4" => LegacyUIGFVersion.Major2Minor3OrHigher,
"v3.0" => LegacyUIGFVersion.Major2Minor3OrHigher,
_ => LegacyUIGFVersion.NotSupported,
};
return version != LegacyUIGFVersion.NotSupported;
}
public bool IsMajor2Minor2OrLowerListValid([NotNullWhen(false)] out long id)
{
foreach (ref readonly LegacyUIGFItem item in CollectionsMarshal.AsSpan(List))
{
if (item.ItemType != SH.ModelInterchangeUIGFItemTypeAvatar && item.ItemType != SH.ModelInterchangeUIGFItemTypeWeapon)
{
id = item.Id;
return false;
}
}
id = 0;
return true;
}
public bool IsMajor2Minor3OrHigherListValid([NotNullWhen(false)] out long id)
{
foreach (ref readonly LegacyUIGFItem item in CollectionsMarshal.AsSpan(List))
{
if (string.IsNullOrEmpty(item.ItemId))
{
id = item.Id;
return false;
}
}
id = 0;
return true;
}
private TimeSpan GetRegionTimeZoneUtcOffset()
{
if (Info.RegionTimeZone is int offsetHours)
{
return new TimeSpan(offsetHours, 0, 0);
}
return PlayerUid.GetRegionTimeZoneUtcOffsetForUid(Info.Uid);
}
}

View File

@@ -1,82 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Service;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.Model.InterChange.GachaLog;
/// <summary>
/// UIGF格式的信息
/// </summary>
[HighQuality]
[Obsolete]
internal sealed class LegacyUIGFInfo : IMappingFrom<LegacyUIGFInfo, RuntimeOptions, CultureOptions, string>
{
/// <summary>
/// 用户Uid
/// </summary>
[JsonPropertyName("uid")]
public string Uid { get; set; } = default!;
/// <summary>
/// 语言
/// </summary>
[JsonPropertyName("lang")]
public string Language { get; set; } = default!;
/// <summary>
/// 导出的时间戳
/// </summary>
[JsonPropertyName("export_timestamp")]
public long? ExportTimestamp { get; set; }
/// <summary>
/// 导出时间
/// </summary>
[JsonIgnore]
public DateTimeOffset ExportDateTime
{
get => DateTimeOffsetExtension.UnsafeRelaxedFromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
}
/// <summary>
/// 导出的 App 名称
/// </summary>
[JsonPropertyName("export_app")]
public string ExportApp { get; set; } = default!;
/// <summary>
/// 导出的 App 版本
/// </summary>
[JsonPropertyName("export_app_version")]
public string ExportAppVersion { get; set; } = default!;
/// <summary>
/// 使用的UIGF版本
/// </summary>
[JsonPropertyName("uigf_version")]
public string UIGFVersion { get; set; } = default!;
/// <summary>
/// 时区偏移
/// </summary>
[JsonPropertyName("region_time_zone")]
public int? RegionTimeZone { get; set; } = default!;
public static LegacyUIGFInfo From(RuntimeOptions runtimeOptions, CultureOptions cultureOptions, string uid)
{
return new()
{
Uid = uid,
Language = cultureOptions.LanguageCode,
ExportTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExportApp = SH.AppName,
ExportAppVersion = runtimeOptions.Version.ToString(),
UIGFVersion = LegacyUIGF.CurrentVersion,
RegionTimeZone = PlayerUid.GetRegionTimeZoneUtcOffsetForUid(uid).Hours,
};
}
}

View File

@@ -1,51 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Json.Annotation;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Metadata.Abstraction;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
namespace Snap.Hutao.Model.InterChange.GachaLog;
/// <summary>
/// UIGF物品
/// </summary>
[HighQuality]
[Obsolete]
internal sealed class LegacyUIGFItem : GachaLogItem, IMappingFrom<LegacyUIGFItem, GachaItem, INameQualityAccess>
{
/// <summary>
/// 额外祈愿映射
/// </summary>
[JsonPropertyName("uigf_gacha_type")]
[JsonEnum(JsonSerializeType.NumberString)]
public GachaType UIGFGachaType { get; set; } = default!;
public static LegacyUIGFItem From(GachaItem item, INameQualityAccess nameQuality)
{
return new()
{
GachaType = item.GachaType,
ItemId = $"{item.ItemId:D}",
Count = 1,
Time = item.Time,
Name = nameQuality.Name,
ItemType = GetItemTypeStringByItemId(item.ItemId),
RankType = nameQuality.Quality,
Id = item.Id,
UIGFGachaType = item.QueryType,
};
}
private static string GetItemTypeStringByItemId(uint itemId)
{
return itemId.StringLength() switch
{
8U => SH.ModelInterchangeUIGFItemTypeAvatar,
5U => SH.ModelInterchangeUIGFItemTypeWeapon,
_ => SH.ModelInterchangeUIGFItemTypeUnknown,
};
}
}

View File

@@ -1,26 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.InterChange.GachaLog;
/// <summary>
/// UIGF版本
/// </summary>
[Obsolete]
internal enum LegacyUIGFVersion
{
/// <summary>
/// 不支持的版本
/// </summary>
NotSupported,
/// <summary>
/// v2.2以及之前的版本
/// </summary>
Major2Minor2OrLower,
/// <summary>
/// v2.3以及之后的版本
/// </summary>
Major2Minor3OrHigher,
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.InterChange;
namespace Snap.Hutao.Model.InterChange.GachaLog;
internal sealed class UIGF
{
@@ -10,7 +10,4 @@ internal sealed class UIGF
[JsonPropertyName("hk4e")]
public List<UIGFEntry<Hk4eItem>>? Hk4e { get; set; }
[JsonPropertyName("snap_hutao_reserved")]
public HutaoReserved? HutaoReserved { get; set; }
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.InterChange;
namespace Snap.Hutao.Model.InterChange.GachaLog;
internal sealed class UIGFEntry<TItem>
{
@@ -16,4 +16,7 @@ internal sealed class UIGFEntry<TItem>
[JsonPropertyName("list")]
public required List<TItem> List { get; set; }
[JsonIgnore]
public bool IsSelected { get; set; }
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.InterChange;
namespace Snap.Hutao.Model.InterChange.GachaLog;
internal sealed class UIGFInfo
{

View File

@@ -1,17 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.InterChange;
internal sealed class HutaoReserved
{
public required uint Version { get; set; }
public List<HutaoReservedEntry<HutaoReservedAchievement>>? Achievement { get; set; }
public List<HutaoReservedEntry<Web.Enka.Model.AvatarInfo>>? AvatarInfo { get; set; }
public List<HutaoReservedEntry<HutaoReservedCultivationEntry>>? Cultivation { get; set; }
public List<HutaoReservedEntry<HutaoReservedSpiralAbyssEntry>>? SpiralAbyss { get; set; }
}

View File

@@ -1,29 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Model.InterChange;
internal sealed class HutaoReservedAchievement : IMappingFrom<HutaoReservedAchievement, Model.Entity.Achievement>
{
public required uint Id { get; set; }
public required uint Current { get; set; }
public required DateTimeOffset Time { get; set; }
public required AchievementStatus Status { get; set; }
public static HutaoReservedAchievement From(Entity.Achievement source)
{
return new()
{
Id = source.Id,
Current = source.Current,
Time = source.Time,
Status = source.Status,
};
}
}

View File

@@ -1,45 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity.Primitive;
namespace Snap.Hutao.Model.InterChange;
internal sealed class HutaoReservedCultivationEntry
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public uint AvatarLevelFrom { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public uint AvatarLevelTo { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public uint SkillALevelFrom { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public uint SkillALevelTo { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public uint SkillELevelFrom { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public uint SkillELevelTo { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public uint SkillQLevelFrom { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public uint SkillQLevelTo { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public uint WeaponLevelFrom { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public uint WeaponLevelTo { get; set; }
public required CultivateType Type { get; set; }
public required uint Id { get; set; }
public required List<HutaoReservedCultivationItem> Items { get; set; }
}

View File

@@ -1,26 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Model.Entity;
namespace Snap.Hutao.Model.InterChange;
internal sealed class HutaoReservedCultivationItem : IMappingFrom<HutaoReservedCultivationItem, CultivateItem>
{
public required uint ItemId { get; set; }
public required uint Count { get; set; }
public required bool IsFinished { get; set; }
public static HutaoReservedCultivationItem From(CultivateItem item)
{
return new()
{
ItemId = item.ItemId,
Count = item.Count,
IsFinished = item.IsFinished,
};
}
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.InterChange;
internal sealed class HutaoReservedEntry<TItem>
{
public required string Identity { get; set; }
public required List<TItem> List { get; set; }
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.InterChange;
internal sealed class HutaoReservedSpiralAbyssEntry
{
public required uint ScheduleId { get; set; }
public required Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss SpiralAbyss { get; set; }
}

View File

@@ -1802,6 +1802,18 @@
<data name="ViewModelSettingSetGamePathDatabaseFailedTitle" xml:space="preserve">
<value>保存游戏路径失败</value>
</data>
<data name="ViewModelUIGFImportDuplicatedHk4eEntry" xml:space="preserve">
<value>导入的 UIGF 文件中包含 UID 重复的祈愿记录项</value>
</data>
<data name="ViewModelUIGFImportNoHk4eEntry" xml:space="preserve">
<value>导入的 UIGF 文件中不包含祈愿数据</value>
</data>
<data name="ViewModelUIGFImportNoSelectedEntry" xml:space="preserve">
<value>请选择至少一个 UID 以导入数据</value>
</data>
<data name="ViewModelUIGFImportSuccess" xml:space="preserve">
<value>导入成功</value>
</data>
<data name="ViewModelUserAdded" xml:space="preserve">
<value>用户 [{0}] 添加成功</value>
</data>

View File

@@ -31,6 +31,28 @@ internal sealed partial class AvatarInfoDbBulkOperation
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
public void UnsafeUpdateDbAvatarInfos(string uid, IEnumerable<EnkaAvatarInfo> webInfos)
{
List<EntityAvatarInfo> dbInfos = avatarInfoDbService.GetAvatarInfoListByUid(uid);
EnsureItemsAvatarIdUnique(ref dbInfos, uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
foreach (EnkaAvatarInfo webInfo in webInfos)
{
if (AvatarIds.IsPlayer(webInfo.AvatarId))
{
continue;
}
EntityAvatarInfo? entity = dbInfoMap.GetValueOrDefault(webInfo.AvatarId);
AddOrUpdateAvatarInfo(entity, uid, appDbContext, webInfo);
}
}
}
public async ValueTask<List<EntityAvatarInfo>> UpdateDbAvatarInfosByShowcaseAsync(string uid, IEnumerable<EnkaAvatarInfo> webInfos, CancellationToken token)
{
await taskContext.SwitchToBackgroundAsync();

View File

@@ -110,4 +110,9 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService
{
this.Delete<GachaItem>(i => i.ArchiveId == archiveId && i.QueryType == queryType && i.Id >= endId);
}
public List<GachaArchive> GetGachaArchiveList()
{
return this.List<GachaArchive>();
}
}

View File

@@ -5,7 +5,6 @@ using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.GachaLog;
using Snap.Hutao.Service.GachaLog.Factory;
using Snap.Hutao.Service.GachaLog.QueryProvider;
using Snap.Hutao.Service.Metadata;
@@ -22,8 +21,6 @@ internal sealed partial class GachaLogService : IGachaLogService
{
private readonly IGachaStatisticsSlimFactory gachaStatisticsSlimFactory;
private readonly IGachaStatisticsFactory gachaStatisticsFactory;
private readonly IUIGFExportService gachaLogExportService;
private readonly IUIGFImportService gachaLogImportService;
private readonly IGachaLogDbService gachaLogDbService;
private readonly IServiceProvider serviceProvider;
private readonly IMetadataService metadataService;
@@ -85,17 +82,6 @@ internal sealed partial class GachaLogService : IGachaLogService
return statistics;
}
public ValueTask<LegacyUIGF> ExportToUIGFAsync(GachaArchive archive)
{
return gachaLogExportService.ExportAsync(context, archive);
}
public async ValueTask ImportFromUIGFAsync(LegacyUIGF uigf)
{
ArgumentNullException.ThrowIfNull(Archives);
await gachaLogImportService.ImportAsync(context, uigf, Archives).ConfigureAwait(false);
}
public async ValueTask<bool> RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategyKind kind, IProgress<GachaLogFetchStatus> progress, CancellationToken token)
{
bool isLazy = kind switch

View File

@@ -22,6 +22,8 @@ internal interface IGachaLogDbService : IAppDbService<GachaArchive>, IAppDbServi
GachaArchive? GetGachaArchiveByUid(string uid);
List<GachaArchive> GetGachaArchiveList();
ObservableCollection<GachaArchive> GetGachaArchiveCollection();
List<GachaItem> GetGachaItemListByArchiveId(Guid archiveId);

View File

@@ -19,56 +19,13 @@ internal interface IGachaLogService
ValueTask<GachaArchive> EnsureArchiveInCollectionAsync(Guid archiveId, CancellationToken token = default(CancellationToken));
/// <summary>
/// 导出为一个新的UIGF对象
/// </summary>
/// <param name="archive">存档</param>
/// <returns>UIGF对象</returns>
ValueTask<LegacyUIGF> ExportToUIGFAsync(GachaArchive archive);
/// <summary>
/// 获得对应的祈愿统计
/// </summary>
/// <param name="archive">存档</param>
/// <returns>祈愿统计</returns>
ValueTask<GachaStatistics> GetStatisticsAsync(GachaArchive archive);
/// <summary>
/// 异步获取简化的祈愿统计列表
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>简化的祈愿统计列表</returns>
ValueTask<List<GachaStatisticsSlim>> GetStatisticsSlimListAsync(CancellationToken token = default);
/// <summary>
/// 异步从UIGF导入数据
/// </summary>
/// <param name="uigf">信息</param>
/// <returns>任务</returns>
ValueTask ImportFromUIGFAsync(LegacyUIGF uigf);
/// <summary>
/// 异步初始化
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>是否初始化成功</returns>
ValueTask<bool> InitializeAsync(CancellationToken token = default);
/// <summary>
/// 刷新祈愿记录
/// 切换选中的存档
/// </summary>
/// <param name="query">查询语句</param>
/// <param name="strategy">刷新策略</param>
/// <param name="progress">进度</param>
/// <param name="token">取消令牌</param>
/// <returns>验证密钥是否有效</returns>
ValueTask<bool> RefreshGachaLogAsync(GachaLogQuery query, RefreshStrategyKind strategy, IProgress<GachaLogFetchStatus> progress, CancellationToken token);
/// <summary>
/// 删除存档
/// </summary>
/// <param name="archive">存档</param>
/// <returns>任务</returns>
ValueTask RemoveArchiveAsync(GachaArchive archive);
}

View File

@@ -1,21 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.GachaLog;
namespace Snap.Hutao.Service.GachaLog;
/// <summary>
/// 祈愿记录导出服务
/// </summary>
internal interface IUIGFExportService
{
/// <summary>
/// 异步导出存档到 UIGF
/// </summary>
/// <param name="context">元数据上下文</param>
/// <param name="archive">存档</param>
/// <returns>UIGF</returns>
ValueTask<LegacyUIGF> ExportAsync(GachaLogServiceMetadataContext context, GachaArchive archive);
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.GachaLog;
namespace Snap.Hutao.Service.GachaLog;
/// <summary>
/// 祈愿记录导入服务
/// </summary>
internal interface IUIGFImportService
{
ValueTask ImportAsync(GachaLogServiceMetadataContext context, LegacyUIGF uigf, AdvancedDbCollectionView<GachaArchive> archives);
}

View File

@@ -13,6 +13,7 @@ internal sealed partial class GachaLogQueryProviderFactory : IGachaLogQueryProvi
public IGachaLogQueryProvider Create(RefreshOption option)
{
// TODO: replace with keyed services
return option switch
{
RefreshOption.SToken => serviceProvider.GetRequiredService<GachaLogQuerySTokenProvider>(),

View File

@@ -1,37 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.GachaLog;
namespace Snap.Hutao.Service.GachaLog;
/// <summary>
/// 祈愿记录导出服务
/// </summary>
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IUIGFExportService))]
internal sealed partial class UIGFExportService : IUIGFExportService
{
private readonly IGachaLogDbService gachaLogDbService;
private readonly RuntimeOptions runtimeOptions;
private readonly CultureOptions cultureOptions;
private readonly ITaskContext taskContext;
/// <inheritdoc/>
public async ValueTask<LegacyUIGF> ExportAsync(GachaLogServiceMetadataContext context, GachaArchive archive)
{
await taskContext.SwitchToBackgroundAsync();
List<GachaItem> entities = gachaLogDbService.GetGachaItemListByArchiveId(archive.InnerId);
List<LegacyUIGFItem> list = entities.SelectList(i => LegacyUIGFItem.From(i, context.GetNameQualityByItemId(i.ItemId)));
LegacyUIGF uigf = new()
{
Info = LegacyUIGFInfo.From(runtimeOptions, cultureOptions, archive.Uid),
List = list,
};
return uigf;
}
}

View File

@@ -1,101 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.GachaLog;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
namespace Snap.Hutao.Service.GachaLog;
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IUIGFImportService))]
internal sealed partial class UIGFImportService : IUIGFImportService
{
private readonly IGachaLogDbService gachaLogDbService;
private readonly ILogger<UIGFImportService> logger;
private readonly CultureOptions cultureOptions;
private readonly ITaskContext taskContext;
public async ValueTask ImportAsync(GachaLogServiceMetadataContext context, LegacyUIGF uigf, AdvancedDbCollectionView<GachaArchive> archives)
{
await taskContext.SwitchToBackgroundAsync();
if (!uigf.IsCurrentVersionSupported(out LegacyUIGFVersion version))
{
HutaoException.InvalidOperation(SH.ServiceUIGFImportUnsupportedVersion);
}
// v2.3+ supports any locale
// v2.2 only supports matched locale
// v2.1 only supports CHS
if (version is LegacyUIGFVersion.Major2Minor2OrLower)
{
if (!cultureOptions.LanguageCodeFitsCurrentLocale(uigf.Info.Language))
{
string message = SH.FormatServiceGachaUIGFImportLanguageNotMatch(uigf.Info.Language, cultureOptions.LanguageCode);
HutaoException.InvalidOperation(message);
}
if (!uigf.IsMajor2Minor2OrLowerListValid(out long id))
{
string message = SH.FormatServiceGachaLogUIGFImportItemInvalidFormat(id);
HutaoException.InvalidOperation(message);
}
}
if (version is LegacyUIGFVersion.Major2Minor3OrHigher)
{
if (!uigf.IsMajor2Minor3OrHigherListValid(out long id))
{
string message = SH.FormatServiceGachaLogUIGFImportItemInvalidFormat(id);
HutaoException.InvalidOperation(message);
}
}
GachaArchiveOperation.GetOrAdd(gachaLogDbService, taskContext, uigf.Info.Uid, archives, out GachaArchive? archive);
Guid archiveId = archive.InnerId;
List<GachaItem> fullItems = [];
foreach (GachaType queryType in GachaLog.QueryTypes)
{
long trimId = gachaLogDbService.GetOldestGachaItemIdByArchiveIdAndQueryType(archiveId, queryType);
logger.LogInformation("Last Id to trim with: [{Id}]", trimId);
List<GachaItem> currentTypedList = version switch
{
LegacyUIGFVersion.Major2Minor3OrHigher => uigf.List
.Where(item => item.UIGFGachaType == queryType && item.Id < trimId)
.OrderByDescending(item => item.Id)
.Select(item => GachaItem.From(archiveId, item))
.ToList(),
LegacyUIGFVersion.Major2Minor2OrLower => uigf.List
.Where(item => item.UIGFGachaType == queryType && item.Id < trimId)
.OrderByDescending(item => item.Id)
.Select(item => GachaItem.From(archiveId, item, context.GetItemId(item)))
.ToList(),
_ => throw HutaoException.NotSupported(),
};
ThrowIfContainsInvalidItem(currentTypedList);
fullItems.AddRange(currentTypedList);
}
gachaLogDbService.AddGachaItemRange(fullItems);
await taskContext.SwitchToMainThreadAsync();
archives.MoveCurrentTo(archive);
}
private static void ThrowIfContainsInvalidItem(List<GachaItem> list)
{
// 越早的记录手工导入的可能性越高
// 错误率相对来说会更高
// 因此从尾部开始查找
if (list.LastOrDefault(item => item.ItemId is 0U) is { } item)
{
HutaoException.InvalidOperation(SH.FormatServiceGachaLogUIGFImportItemInvalidFormat(item.Id));
}
}
}

View File

@@ -8,50 +8,22 @@ using Snap.Hutao.Service.Game.Configuration;
namespace Snap.Hutao.Service.Game;
/// <summary>
/// 游戏服务
/// </summary>
[HighQuality]
internal interface IGameServiceFacade
{
/// <summary>
/// 游戏内账号集合
/// </summary>
ObservableReorderableDbCollection<GameAccount> GameAccountCollection { get; }
ValueTask AttachGameAccountToUidAsync(GameAccount gameAccount, string uid);
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme);
/// <summary>
/// 异步获取游戏路径
/// </summary>
/// <returns>结果</returns>
ValueTask<ValueResult<bool, string>> GetGamePathAsync();
/// <summary>
/// 获取多通道值
/// </summary>
/// <returns>多通道值</returns>
ChannelOptions GetChannelOptions();
/// <summary>
/// 游戏是否正在运行
/// </summary>
/// <returns>是否正在运行</returns>
bool IsGameRunning();
/// <summary>
/// 异步修改游戏账号名称
/// </summary>
/// <param name="gameAccount">游戏账号</param>
/// <returns>任务</returns>
ValueTask ModifyGameAccountAsync(GameAccount gameAccount);
/// <summary>
/// 异步尝试移除账号
/// </summary>
/// <param name="gameAccount">账号</param>
/// <returns>任务</returns>
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
GameAccount? DetectCurrentGameAccount(SchemeType scheme);

View File

@@ -5,5 +5,5 @@ namespace Snap.Hutao.Service.UIGF;
internal interface IUIGFExportService
{
ValueTask<bool> ExportAsync(UIGFExportOptions exportOptions, CancellationToken token);
ValueTask ExportAsync(UIGFExportOptions exportOptions, CancellationToken token);
}

View File

@@ -5,5 +5,5 @@ namespace Snap.Hutao.Service.UIGF;
internal interface IUIGFImportService
{
ValueTask<bool> ImportAsync(UIGFImportOptions importOptions, CancellationToken token);
ValueTask ImportAsync(UIGFImportOptions importOptions, CancellationToken token);
}

View File

@@ -5,5 +5,7 @@ namespace Snap.Hutao.Service.UIGF;
internal interface IUIGFService
{
ValueTask<bool> ExportAsync(UIGFExportOptions exportOptions, CancellationToken token);
ValueTask ExportAsync(UIGFExportOptions exportOptions, CancellationToken token = default);
ValueTask ImportAsync(UIGFImportOptions importOptions, CancellationToken token = default);
}

View File

@@ -3,14 +3,9 @@
using Snap.Hutao.Core;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange;
using Snap.Hutao.Service.Achievement;
using Snap.Hutao.Service.AvatarInfo;
using Snap.Hutao.Service.Cultivation;
using Snap.Hutao.Model.InterChange.GachaLog;
using Snap.Hutao.Service.GachaLog;
using Snap.Hutao.Service.SpiralAbyss;
using System.IO;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Service.UIGF;
@@ -23,11 +18,11 @@ internal sealed partial class UIGF40ExportService : IUIGFExportService
private readonly RuntimeOptions runtimeOptions;
private readonly ITaskContext taskContext;
public async ValueTask<bool> ExportAsync(UIGFExportOptions exportOptions, CancellationToken token)
public async ValueTask ExportAsync(UIGFExportOptions exportOptions, CancellationToken token = default)
{
await taskContext.SwitchToBackgroundAsync();
Model.InterChange.UIGF uigf = new()
Model.InterChange.GachaLog.UIGF uigf = new()
{
Info = new()
{
@@ -36,27 +31,17 @@ internal sealed partial class UIGF40ExportService : IUIGFExportService
ExportTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(),
Version = "v4.0",
},
HutaoReserved = new()
{
Version = 1,
},
};
ExportGachaArchives(uigf, exportOptions.GachaArchiveIds);
ExportAchievementArchives(uigf.HutaoReserved, exportOptions.ReservedAchievementArchiveIds);
ExportAvatarInfoUids(uigf.HutaoReserved, exportOptions.ReservedAvatarInfoUids);
ExportCultivationProjects(uigf.HutaoReserved, exportOptions.ReservedCultivationProjectIds);
ExportSpialAbysses(uigf.HutaoReserved, exportOptions.ReservedSpiralAbyssUids);
using (FileStream stream = File.Create(exportOptions.FilePath))
{
await JsonSerializer.SerializeAsync(stream, uigf, jsonOptions, token).ConfigureAwait(false);
}
return true;
}
private void ExportGachaArchives(Model.InterChange.UIGF uigf, List<Guid> archiveIds)
private void ExportGachaArchives(Model.InterChange.GachaLog.UIGF uigf, List<Guid> archiveIds)
{
if (archiveIds.Count <= 0)
{
@@ -83,138 +68,4 @@ internal sealed partial class UIGF40ExportService : IUIGFExportService
uigf.Hk4e = results;
}
private void ExportAchievementArchives(HutaoReserved hutaoReserved, List<Guid> archiveIds)
{
if (archiveIds.Count <= 0)
{
return;
}
IAchievementDbService achievementDbService = serviceProvider.GetRequiredService<IAchievementDbService>();
List<HutaoReservedEntry<HutaoReservedAchievement>> results = [];
foreach (Guid archiveId in archiveIds)
{
AchievementArchive? archive = achievementDbService.GetAchievementArchiveById(archiveId);
ArgumentNullException.ThrowIfNull(archive);
List<Model.Entity.Achievement> dbItems = achievementDbService.GetAchievementListByArchiveId(archiveId);
HutaoReservedEntry<HutaoReservedAchievement> entry = new()
{
Identity = archive.Name,
List = dbItems.SelectList(HutaoReservedAchievement.From),
};
results.Add(entry);
}
hutaoReserved.Achievement = results;
}
private void ExportAvatarInfoUids(HutaoReserved hutaoReserved, List<string> uids)
{
if (uids.Count <= 0)
{
return;
}
IAvatarInfoDbService avatarInfoDbService = serviceProvider.GetRequiredService<IAvatarInfoDbService>();
List<HutaoReservedEntry<Web.Enka.Model.AvatarInfo>> results = [];
foreach (string uid in uids)
{
List<Model.Entity.AvatarInfo>? dbItems = avatarInfoDbService.GetAvatarInfoListByUid(uid);
HutaoReservedEntry<Web.Enka.Model.AvatarInfo> entry = new()
{
Identity = uid,
List = dbItems.SelectList(item => item.Info),
};
results.Add(entry);
}
hutaoReserved.AvatarInfo = results;
}
private void ExportCultivationProjects(HutaoReserved hutaoReserved, List<Guid> projectIds)
{
if (projectIds.Count <= 0)
{
return;
}
ICultivationDbService cultivationDbService = serviceProvider.GetRequiredService<ICultivationDbService>();
List<HutaoReservedEntry<HutaoReservedCultivationEntry>> results = [];
foreach (Guid projectId in projectIds)
{
CultivateProject? project = cultivationDbService.GetCultivateProjectById(projectId);
ArgumentNullException.ThrowIfNull(project);
List<CultivateEntry> entries = cultivationDbService.GetCultivateEntryListIncludingLevelInformationByProjectId(projectId);
List<HutaoReservedCultivationEntry> innerResults = [];
foreach (ref readonly CultivateEntry innerEntry in CollectionsMarshal.AsSpan(entries))
{
List<CultivateItem> items = cultivationDbService.GetCultivateItemListByEntryId(innerEntry.InnerId);
HutaoReservedCultivationEntry innerResultEntry = new()
{
AvatarLevelFrom = innerEntry.LevelInformation?.AvatarLevelFrom ?? 0,
AvatarLevelTo = innerEntry.LevelInformation?.AvatarLevelTo ?? 0,
SkillALevelFrom = innerEntry.LevelInformation?.SkillALevelFrom ?? 0,
SkillALevelTo = innerEntry.LevelInformation?.SkillALevelTo ?? 0,
SkillELevelFrom = innerEntry.LevelInformation?.SkillELevelFrom ?? 0,
SkillELevelTo = innerEntry.LevelInformation?.SkillELevelTo ?? 0,
SkillQLevelFrom = innerEntry.LevelInformation?.SkillQLevelFrom ?? 0,
SkillQLevelTo = innerEntry.LevelInformation?.SkillQLevelTo ?? 0,
WeaponLevelFrom = innerEntry.LevelInformation?.WeaponLevelFrom ?? 0,
WeaponLevelTo = innerEntry.LevelInformation?.WeaponLevelTo ?? 0,
Type = innerEntry.Type,
Id = innerEntry.Id,
Items = items.SelectList(HutaoReservedCultivationItem.From),
};
innerResults.Add(innerResultEntry);
}
HutaoReservedEntry<HutaoReservedCultivationEntry> outerEntry = new()
{
Identity = project.Name,
List = innerResults,
};
results.Add(outerEntry);
}
hutaoReserved.Cultivation = results;
}
private void ExportSpialAbysses(HutaoReserved hutaoReserved, List<string> uids)
{
if (uids.Count <= 0)
{
return;
}
ISpiralAbyssRecordDbService spiralAbyssRecordDbService = serviceProvider.GetRequiredService<ISpiralAbyssRecordDbService>();
List<HutaoReservedEntry<HutaoReservedSpiralAbyssEntry>> results = [];
foreach (string uid in uids)
{
Dictionary<uint, SpiralAbyssEntry> dbMap = spiralAbyssRecordDbService.GetSpiralAbyssEntryMapByUid(uid);
HutaoReservedEntry<HutaoReservedSpiralAbyssEntry> entry = new()
{
Identity = uid,
List = dbMap.Select(item => new HutaoReservedSpiralAbyssEntry
{
ScheduleId = item.Key,
SpiralAbyss = item.Value.SpiralAbyss,
}).ToList(),
};
results.Add(entry);
}
hutaoReserved.SpiralAbyss = results;
}
}

View File

@@ -1,10 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange;
using Snap.Hutao.Service.Achievement;
using Snap.Hutao.Model.InterChange.GachaLog;
using Snap.Hutao.Service.GachaLog;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
@@ -14,18 +12,13 @@ namespace Snap.Hutao.Service.UIGF;
[Injection(InjectAs.Transient, typeof(IUIGFImportService), Key = UIGFVersion.UIGF40)]
internal sealed partial class UIGF40ImportService : IUIGFImportService
{
private readonly JsonSerializerOptions jsonOptions;
private readonly IServiceProvider serviceProvider;
private readonly RuntimeOptions runtimeOptions;
private readonly ITaskContext taskContext;
public async ValueTask<bool> ImportAsync(UIGFImportOptions importOptions, CancellationToken token)
public async ValueTask ImportAsync(UIGFImportOptions importOptions, CancellationToken token = default)
{
await taskContext.SwitchToBackgroundAsync();
ImportGachaArchives(importOptions.UIGF.Hk4e, importOptions.GachaArchiveUids);
ImportAchievementArchives(importOptions.UIGF.HutaoReserved?.Achievement, importOptions.ReservedAchievementArchiveIdentities);
return true;
}
private void ImportGachaArchives(List<UIGFEntry<Hk4eItem>>? entries, HashSet<string> uids)
@@ -70,36 +63,4 @@ internal sealed partial class UIGF40ImportService : IUIGFImportService
gachaLogDbService.AddGachaItemRange(fullItems);
}
}
private void ImportAchievementArchives(List<HutaoReservedEntry<HutaoReservedAchievement>>? entries, HashSet<string> identities)
{
if (entries.IsNullOrEmpty() || identities.IsNullOrEmpty())
{
return;
}
IAchievementDbService achievementDbService = serviceProvider.GetRequiredService<IAchievementDbService>();
AchievementDbBulkOperation achievementDbBulkOperation = serviceProvider.GetRequiredService<AchievementDbBulkOperation>();
foreach (HutaoReservedEntry<HutaoReservedAchievement> entry in entries)
{
if (!identities.Contains(entry.Identity))
{
continue;
}
AchievementArchive? archive = achievementDbService.GetAchievementArchiveByName(entry.Identity);
if (archive is null)
{
archive = AchievementArchive.From(entry.Identity);
achievementDbService.AddAchievementArchive(archive);
}
Guid archiveId = archive.InnerId;
List<Model.Entity.Achievement> achievements = entry.List.SelectList(item => Model.Entity.Achievement.From(archiveId, item));
achievementDbBulkOperation.Overwrite(archiveId, achievements);
}
}
}
}

View File

@@ -8,12 +8,4 @@ internal sealed class UIGFExportOptions
public required string FilePath { get; set; }
public required List<Guid> GachaArchiveIds { get; set; }
public required List<Guid> ReservedAchievementArchiveIds { get; set; }
public required List<string> ReservedAvatarInfoUids { get; set; }
public required List<Guid> ReservedCultivationProjectIds { get; set; }
public required List<string> ReservedSpiralAbyssUids { get; set; }
}

View File

@@ -5,15 +5,7 @@ namespace Snap.Hutao.Service.UIGF;
internal sealed class UIGFImportOptions
{
public required Model.InterChange.UIGF UIGF { get; set; }
public required Model.InterChange.GachaLog.UIGF UIGF { get; set; }
public required HashSet<string> GachaArchiveUids { get; set; }
public required HashSet<string> ReservedAchievementArchiveIdentities { get; set; }
public required HashSet<string> ReservedAvatarInfoIdentities { get; set; }
public required HashSet<string> ReservedCultivationProjectIdentities { get; set; }
public required HashSet<string> ReservedSpiralAbyssIdentities { get; set; }
}

View File

@@ -4,14 +4,20 @@
namespace Snap.Hutao.Service.UIGF;
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IUIGFService))]
[Injection(InjectAs.Singleton, typeof(IUIGFService))]
internal sealed partial class UIGFService : IUIGFService
{
private readonly IServiceProvider serviceProvider;
public ValueTask<bool> ExportAsync(UIGFExportOptions exportOptions, CancellationToken token)
public ValueTask ExportAsync(UIGFExportOptions exportOptions, CancellationToken token = default)
{
IUIGFExportService exportService = serviceProvider.GetRequiredKeyedService<IUIGFExportService>(UIGFVersion.UIGF40);
return exportService.ExportAsync(exportOptions, token);
}
public ValueTask ImportAsync(UIGFImportOptions importOptions, CancellationToken token = default)
{
IUIGFImportService importService = serviceProvider.GetRequiredKeyedService<IUIGFImportService>(UIGFVersion.UIGF40);
return importService.ImportAsync(importOptions, token);
}
}

View File

@@ -150,6 +150,7 @@
<None Remove="Resource\WelcomeView_Background.png" />
<None Remove="Service\Game\Automation\ScreenCapture\GameScreenCaptureDebugPreviewWindow.xaml" />
<None Remove="stylecop.json" />
<None Remove="UI\Xaml\View\Dialog\UIGFImportDialog.xaml" />
<None Remove="UI\Xaml\View\Window\WebView2Window.xaml" />
<None Remove="UI\Xaml\View\Dialog\AchievementArchiveCreateDialog.xaml" />
<None Remove="UI\Xaml\View\Dialog\AchievementImportDialog.xaml" />
@@ -339,6 +340,11 @@
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnablePreviewMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<Page Update="UI\Xaml\View\Dialog\UIGFImportDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="UI\Xaml\Control\StandardView.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -38,4 +38,13 @@ internal sealed partial class UInt32ToGradientColorConverter : DependencyValueCo
color.B = (byte)b;
return color;
}
}
internal sealed class TimestampToLocalTimeStringConverter : ValueConverter<long, string>
{
public override string Convert(long from)
{
DateTimeOffset dto = DateTimeOffset.FromUnixTimeSeconds(from).ToLocalTime();
return $"{dto:yyyy-MM-dd HH:mm:ss}";
}
}

View File

@@ -0,0 +1,66 @@
<ContentDialog
x:Class="Snap.Hutao.UI.Xaml.View.Dialog.UIGFImportDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cwc="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Snap.Hutao.UI.Xaml.View.Dialog"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shmig="using:Snap.Hutao.Model.InterChange.GachaLog"
xmlns:shuxdcs="using:Snap.Hutao.UI.Xaml.Data.Converter.Specialized"
xmlns:shuxm="using:Snap.Hutao.UI.Xaml.Markup"
mc:Ignorable="d">
<ContentDialog.Resources>
<shuxdcs:TimestampToLocalTimeStringConverter x:Key="TimestampToLocalTimeStringConverter"/>
</ContentDialog.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<cwc:UniformGrid
Grid.Row="0"
ColumnSpacing="16"
Columns="3"
RowSpacing="16">
<cwc:HeaderedContentControl Header="{shuxm:ResourceString Name=ViewDialogImportExportApp}">
<TextBlock
Margin="0,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind UIGF.Info.ExportApp, Mode=OneWay}"/>
</cwc:HeaderedContentControl>
<cwc:HeaderedContentControl Header="{shuxm:ResourceString Name=ViewDialogImportExportTime}">
<TextBlock
Margin="0,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind UIGF.Info.ExportTimestamp, Converter={StaticResource TimestampToLocalTimeStringConverter}, Mode=OneWay}"/>
</cwc:HeaderedContentControl>
<cwc:HeaderedContentControl Header="{shuxm:ResourceString Name=ViewDialogImportExportAppVersion}">
<TextBlock
Margin="0,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind UIGF.Info.ExportAppVersion, Mode=OneWay}"/>
</cwc:HeaderedContentControl>
<cwc:HeaderedContentControl Header="{shuxm:ResourceString Name=ViewDialogImportUIGFExportUIGFVersion}">
<TextBlock
Margin="0,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind UIGF.Info.Version, Mode=OneWay}"/>
</cwc:HeaderedContentControl>
<ItemsControl ItemsSource="{x:Bind Selections}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Uid}" IsChecked="{Binding IsSelected, Mode=TwoWay}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</cwc:UniformGrid>
</Grid>
</ContentDialog>

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Model.InterChange.GachaLog;
namespace Snap.Hutao.UI.Xaml.View.Dialog;
[DependencyProperty("UIGF", typeof(UIGF))]
[DependencyProperty("Selections", typeof(List<UIGFUidSelection>))]
internal sealed partial class UIGFImportDialog : ContentDialog
{
private readonly ITaskContext taskContext;
public UIGFImportDialog(IServiceProvider serviceProvider, UIGF uigf)
{
InitializeComponent();
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
UIGF = uigf;
Selections = uigf.Hk4e?.SelectList(item => new UIGFUidSelection(item.Uid));
}
public async ValueTask<ValueResult<bool, HashSet<string>>> GetSelectedUidsAsync()
{
await taskContext.SwitchToMainThreadAsync();
if (await ShowAsync() is ContentDialogResult.Primary)
{
HashSet<string> uids = Selections.Where(item => item.IsSelected).Select(item => item.Uid).ToHashSet();
return new(true, uids);
}
return new(false, default!);
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Model.InterChange.GachaLog;
namespace Snap.Hutao.UI.Xaml.View.Dialog;
internal sealed class UIGFUidSelection : ObservableObject
{
private bool isSelected = true;
public UIGFUidSelection(string uid)
{
Uid = uid;
}
public bool IsSelected { get => isSelected; set => SetProperty(ref isSelected, value); }
public string Uid { get; set; }
}

View File

@@ -114,6 +114,7 @@
Padding="16"
Spacing="16">
<!-- 关于 -->
<Border x:Name="ScrollViwerTopPanel" cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}" Visibility="Visible">
@@ -193,43 +194,40 @@
</Border>
</Border>
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<!-- 胡桃通行证 -->
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}" DataContext="{Binding Passport}">
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
<StackPanel Spacing="{ThemeResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource SettingsCardHeaderTextBlockStyle}" Text="{shuxm:ResourceString Name=ViewPageSettingCardHutaoPassportHeader}"/>
<!--
https://github.com/DGP-Studio/Snap.Hutao/issues/1072
ItemsRepeater will behave abnormal if no direct scrollhost wrapping it
-->
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Disabled">
<cwc:SettingsExpander
Description="{Binding UserOptions.UserName}"
Description="{Binding User.UserName}"
Header="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportHeader}"
HeaderIcon="{shuxm:FontIcon Glyph=&#xE716;}"
IsExpanded="True">
<StackPanel Orientation="Horizontal" Spacing="16">
<Button
Command="{Binding Passport.LoginCommand}"
Command="{Binding LoginCommand}"
Content="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportLoginAction}"
Style="{ThemeResource SettingButtonStyle}"
Visibility="{Binding UserOptions.IsLoggedIn, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
Visibility="{Binding User.IsLoggedIn, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<Button
Command="{Binding Passport.RegisterCommand}"
Command="{Binding RegisterCommand}"
Content="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportRegisterAction}"
Style="{ThemeResource SettingButtonStyle}"
Visibility="{Binding UserOptions.IsLoggedIn, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
Visibility="{Binding User.IsLoggedIn, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<Button
Command="{Binding Passport.LogoutCommand}"
Command="{Binding LogoutCommand}"
Content="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportLogoutAction}"
Style="{ThemeResource SettingButtonStyle}"
Visibility="{Binding UserOptions.IsLoggedIn, Converter={StaticResource BoolToVisibilityConverter}}"/>
Visibility="{Binding User.IsLoggedIn, Converter={StaticResource BoolToVisibilityConverter}}"/>
</StackPanel>
<cwc:SettingsExpander.Items>
<cwc:SettingsCard
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
Description="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperDescription}"
Header="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperHeader}"
Visibility="{Binding UserOptions.IsLicensedDeveloper, Converter={StaticResource BoolToVisibilityConverter}}">
Visibility="{Binding User.IsLicensedDeveloper, Converter={StaticResource BoolToVisibilityConverter}}">
<Button
Command="{Binding OpenTestPageCommand}"
Content="TEST"
@@ -239,10 +237,10 @@
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
Description="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportMaintainerDescription}"
Header="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportMaintainerHeader}"
Visibility="{Binding UserOptions.IsMaintainer, Converter={StaticResource BoolToVisibilityConverter}}"/>
<cwc:SettingsCard Description="{Binding UserOptions.GachaLogExpireAtSlim}" Header="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportGachaLogExpiredAtHeader}"/>
Visibility="{Binding User.IsMaintainer, Converter={StaticResource BoolToVisibilityConverter}}"/>
<cwc:SettingsCard Description="{Binding User.GachaLogExpireAtSlim}" Header="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportGachaLogExpiredAtHeader}"/>
<cwc:SettingsCard
Command="{Binding Passport.OpenRedeemWebsiteCommand}"
Command="{Binding OpenRedeemWebsiteCommand}"
Description="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportRedeemCodeDescription}"
Header="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportRedeemCodeHeader}"
IsClickEnabled="True"/>
@@ -252,11 +250,11 @@
Header="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportDangerZoneHeader}">
<StackPanel Orientation="Horizontal" Spacing="16">
<Button
Command="{Binding Passport.ResetPasswordCommand}"
Command="{Binding ResetPasswordCommand}"
Content="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportResetPasswordAction}"
Style="{ThemeResource SettingButtonStyle}"/>
<Button
Command="{Binding Passport.UnregisterCommand}"
Command="{Binding UnregisterCommand}"
Content="{shuxm:ResourceString Name=ViewPageSettingHutaoPassportUnregisterAction}"
Style="{ThemeResource SettingButtonStyle}"/>
</StackPanel>
@@ -268,7 +266,8 @@
</Border>
</Border>
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<!-- 无感验证 -->
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}" DataContext="{Binding Geetest}">
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
<StackPanel Spacing="{ThemeResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource SettingsCardHeaderTextBlockStyle}" Text="{shuxm:ResourceString Name=ViewPageSettingGeetestVerificationHeader}"/>
@@ -284,7 +283,8 @@
</Border>
</Border>
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<!-- 外观 -->
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}" DataContext="{Binding Appearance}">
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
<StackPanel Spacing="{ThemeResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource SettingsCardHeaderTextBlockStyle}" Text="{shuxm:ResourceString Name=ViewPageSettingApperanceHeader}"/>
@@ -357,7 +357,8 @@
</Border>
</Border>
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<!-- 存储空间 -->
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}" DataContext="{Binding Storage}">
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
<StackPanel Spacing="{ThemeResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource SettingsCardHeaderTextBlockStyle}" Text="{shuxm:ResourceString Name=ViewPageSettingStorageHeader}"/>
@@ -418,7 +419,8 @@
</Border>
</Border>
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<!-- 快捷键 -->
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}" DataContext="{Binding HotKey}">
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
<StackPanel Spacing="{ThemeResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource SettingsCardHeaderTextBlockStyle}" Text="{shuxm:ResourceString Name=ViewPageSettingKeyShortcutHeader}"/>
@@ -486,7 +488,8 @@
</Border>
</Border>
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<!-- 主页 -->
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}" DataContext="{Binding Home}">
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
<StackPanel Spacing="{ThemeResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource SettingsCardHeaderTextBlockStyle}" Text="{shuxm:ResourceString Name=ViewpageSettingHomeHeader}"/>
@@ -537,7 +540,8 @@
</Border>
</Border>
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<!-- 游戏 -->
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}" DataContext="{Binding Game}">
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
<StackPanel Spacing="{ThemeResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource SettingsCardHeaderTextBlockStyle}" Text="{shuxm:ResourceString Name=ViewPageSettingGameHeader}"/>
@@ -553,7 +557,8 @@
</Border>
</Border>
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<!-- 祈愿记录 -->
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}" DataContext="{Binding GachaLog}">
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
<StackPanel Spacing="{ThemeResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource SettingsCardHeaderTextBlockStyle}" Text="{shuxm:ResourceString Name=ViewPageSettingGachaLogHeader}"/>
@@ -579,7 +584,8 @@
</Border>
</Border>
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<!-- 危险功能 -->
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}" DataContext="{Binding DangerousFeature}">
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
<StackPanel Spacing="{ThemeResource SettingsCardSpacing}">
<TextBlock

View File

@@ -229,58 +229,6 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
}
}
[Command("ImportFromUIGFJsonCommand")]
private async Task ImportFromUIGFJsonAsync()
{
(bool isOk, ValueFile file) = fileSystemPickerInteraction.PickFile(
SH.ViewModelGachaUIGFImportPickerTitile,
[(SH.ViewModelGachaLogExportFileType, "*.json")]);
if (!isOk)
{
return;
}
ValueResult<bool, LegacyUIGF?> result = await file.DeserializeFromJsonAsync<LegacyUIGF>(options).ConfigureAwait(false);
if (result.TryGetValue(out LegacyUIGF? uigf))
{
await TryImportUIGFInternalAsync(uigf).ConfigureAwait(false);
}
else
{
infoBarService.Error(SH.ViewModelImportWarningTitle, SH.ViewModelImportWarningMessage);
}
}
[Command("ExportToUIGFJsonCommand")]
private async Task ExportToUIGFJsonAsync()
{
if (Archives?.CurrentItem is null)
{
return;
}
(bool isOk, ValueFile file) = fileSystemPickerInteraction.SaveFile(
SH.ViewModelGachaLogUIGFExportPickerTitle,
$"{Archives.CurrentItem.Uid}.json",
[(SH.ViewModelGachaLogExportFileType, "*.json")]);
if (!isOk)
{
return;
}
LegacyUIGF uigf = await gachaLogService.ExportToUIGFAsync(Archives.CurrentItem).ConfigureAwait(false);
if (await file.SerializeToJsonAsync(uigf, options).ConfigureAwait(false))
{
infoBarService.Success(SH.ViewModelExportSuccessTitle, SH.ViewModelExportSuccessMessage);
}
else
{
infoBarService.Warning(SH.ViewModelExportWarningTitle, SH.ViewModelExportWarningMessage);
}
}
[Command("RemoveArchiveCommand")]
private async Task RemoveArchiveAsync()
{
@@ -347,52 +295,4 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
infoBarService.Error(ex);
}
}
private async ValueTask<bool> TryImportUIGFInternalAsync(LegacyUIGF uigf)
{
if (!uigf.IsCurrentVersionSupported(out _))
{
infoBarService.Warning(SH.ViewModelGachaLogImportWarningTitle, SH.ViewModelGachaLogImportWarningMessage);
return false;
}
GachaLogImportDialog importDialog = await contentDialogFactory.CreateInstanceAsync<GachaLogImportDialog>(uigf).ConfigureAwait(false);
if (!await importDialog.GetShouldImportAsync().ConfigureAwait(false))
{
return false;
}
await taskContext.SwitchToMainThreadAsync();
ContentDialog dialog = await contentDialogFactory.CreateForIndeterminateProgressAsync(SH.ViewModelGachaLogImportProgress).ConfigureAwait(true);
try
{
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{
try
{
suppressCurrentItemChangedHandling = true;
await gachaLogService.ImportFromUIGFAsync(uigf).ConfigureAwait(false);
}
finally
{
suppressCurrentItemChangedHandling = false;
await UpdateStatisticsAsync(Archives?.CurrentItem).ConfigureAwait(false);
}
}
}
catch (InvalidOperationException ex)
{
// 语言不匹配/导入物品中存在无效的项
infoBarService.Error(ex);
return false;
}
catch (FormatException ex)
{
infoBarService.Error(ex);
return false;
}
infoBarService.Success(SH.ViewModelGachaLogImportComplete);
return true;
}
}

View File

@@ -3,8 +3,10 @@
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.UI.Xaml.View.Dialog;
using Snap.Hutao.UI.Xaml.View.Page;
using Snap.Hutao.Web;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.Response;
@@ -12,51 +14,58 @@ using Windows.System;
namespace Snap.Hutao.ViewModel.Setting;
/// <summary>
/// 胡桃通行证视图模型
/// </summary>
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly INavigationService navigationService;
private readonly HutaoUserOptions hutaoUserOptions;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
public HutaoUserOptions User { get => hutaoUserOptions; }
[Command("OpenRedeemWebsiteCommand")]
private static async Task OpenRedeemWebsiteAsync()
{
await Launcher.LaunchUriAsync(HutaoEndpoints.Website("redeem.html").ToUri());
}
[Command("OpenTestPageCommand")]
private async Task OpenTestPageAsync()
{
await navigationService.NavigateAsync<TestPage>(INavigationAwaiter.Default).ConfigureAwait(false);
}
[Command("RegisterCommand")]
private async Task RegisterAsync()
{
HutaoPassportRegisterDialog dialog = await contentDialogFactory.CreateInstanceAsync<HutaoPassportRegisterDialog>().ConfigureAwait(false);
ValueResult<bool, (string UserName, string Password, string VerifyCode)> result = await dialog.GetInputAsync().ConfigureAwait(false);
if (result.IsOk)
if (!result.IsOk)
{
(string username, string password, string verifyCode) = result.Value;
return;
}
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(verifyCode))
(string username, string password, string verifyCode) = result.Value;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(verifyCode))
{
return;
}
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
HutaoResponse<string> response = await hutaoPassportClient.RegisterAsync(username, password, verifyCode).ConfigureAwait(false);
if (response.IsOk())
{
return;
}
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
HutaoResponse<string> response = await hutaoPassportClient.RegisterAsync(username, password, verifyCode).ConfigureAwait(false);
if (response.IsOk())
{
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await hutaoUserOptions.PostLoginSucceedAsync(hutaoPassportClient, taskContext, username, password, response.Data).ConfigureAwait(false);
}
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await hutaoUserOptions.PostLoginSucceedAsync(hutaoPassportClient, taskContext, username, password, response.Data).ConfigureAwait(false);
}
}
}
@@ -67,28 +76,29 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel
HutaoPassportUnregisterDialog dialog = await contentDialogFactory.CreateInstanceAsync<HutaoPassportUnregisterDialog>().ConfigureAwait(false);
ValueResult<bool, (string UserName, string Password, string VerifyCode)> result = await dialog.GetInputAsync().ConfigureAwait(false);
if (result.IsOk)
if (!result.IsOk)
{
(string username, string password, string verifyCode) = result.Value;
return;
}
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(verifyCode))
(string username, string password, string verifyCode) = result.Value;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(verifyCode))
{
return;
}
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
HutaoResponse response = await hutaoPassportClient.UnregisterAsync(username, password, verifyCode).ConfigureAwait(false);
if (response.IsOk())
{
return;
}
infoBarService.Information(response.GetLocalizationMessageOrMessage());
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
HutaoResponse response = await hutaoPassportClient.UnregisterAsync(username, password, verifyCode).ConfigureAwait(false);
if (response.IsOk())
{
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await taskContext.SwitchToMainThreadAsync();
hutaoUserOptions.PostLogoutOrUnregister();
}
await taskContext.SwitchToMainThreadAsync();
hutaoUserOptions.PostLogoutOrUnregister();
}
}
}
@@ -99,26 +109,27 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel
HutaoPassportLoginDialog dialog = await contentDialogFactory.CreateInstanceAsync<HutaoPassportLoginDialog>().ConfigureAwait(false);
ValueResult<bool, (string UserName, string Password)> result = await dialog.GetInputAsync().ConfigureAwait(false);
if (result.IsOk)
if (!result.IsOk)
{
(string username, string password) = result.Value;
return;
}
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
(string username, string password) = result.Value;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
return;
}
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
HutaoResponse<string> response = await hutaoPassportClient.LoginAsync(username, password).ConfigureAwait(false);
if (response.IsOk())
{
return;
}
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
HutaoResponse<string> response = await hutaoPassportClient.LoginAsync(username, password).ConfigureAwait(false);
if (response.IsOk())
{
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await hutaoUserOptions.PostLoginSucceedAsync(hutaoPassportClient, taskContext, username, password, response.Data).ConfigureAwait(false);
}
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await hutaoUserOptions.PostLoginSucceedAsync(hutaoPassportClient, taskContext, username, password, response.Data).ConfigureAwait(false);
}
}
}
@@ -135,26 +146,27 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel
HutaoPassportResetPasswordDialog dialog = await contentDialogFactory.CreateInstanceAsync<HutaoPassportResetPasswordDialog>().ConfigureAwait(false);
ValueResult<bool, (string UserName, string Password, string VerifyCode)> result = await dialog.GetInputAsync().ConfigureAwait(false);
if (result.IsOk)
if (!result.IsOk)
{
(string username, string password, string verifyCode) = result.Value;
return;
}
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(verifyCode))
(string username, string password, string verifyCode) = result.Value;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(verifyCode))
{
return;
}
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
HutaoResponse<string> response = await hutaoPassportClient.ResetPasswordAsync(username, password, verifyCode).ConfigureAwait(false);
if (response.IsOk())
{
return;
}
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
HutaoResponse<string> response = await hutaoPassportClient.ResetPasswordAsync(username, password, verifyCode).ConfigureAwait(false);
if (response.IsOk())
{
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await hutaoUserOptions.PostLoginSucceedAsync(hutaoPassportClient, taskContext, username, password, response.Data).ConfigureAwait(false);
}
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await hutaoUserOptions.PostLoginSucceedAsync(hutaoPassportClient, taskContext, username, password, response.Data).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,81 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Model;
using Snap.Hutao.Service;
using Snap.Hutao.Service.BackgroundImage;
using Snap.Hutao.UI.Xaml.Media.Backdrop;
using System.Globalization;
namespace Snap.Hutao.ViewModel.Setting;
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class SettingAppearanceViewModel : Abstraction.ViewModel
{
private readonly BackgroundImageOptions backgroundImageOptions;
private readonly CultureOptions cultureOptions;
private readonly AppOptions appOptions;
private NameValue<CultureInfo>? selectedCulture;
private NameValue<BackdropType>? selectedBackdropType;
private NameValue<ElementTheme>? selectedElementTheme;
private NameValue<BackgroundImageType>? selectedBackgroundImageType;
public CultureOptions CultureOptions { get => cultureOptions; }
public AppOptions AppOptions { get => appOptions; }
public BackgroundImageOptions BackgroundImageOptions { get => backgroundImageOptions; }
public NameValue<CultureInfo>? SelectedCulture
{
get => selectedCulture ??= CultureOptions.GetCurrentCultureForSelectionOrDefault();
set
{
if (SetProperty(ref selectedCulture, value) && value is not null)
{
CultureOptions.CurrentCulture = value.Value;
AppInstance.Restart(string.Empty);
}
}
}
public NameValue<BackdropType>? SelectedBackdropType
{
get => selectedBackdropType ??= AppOptions.BackdropTypes.Single(t => t.Value == AppOptions.BackdropType);
set
{
if (SetProperty(ref selectedBackdropType, value) && value is not null)
{
AppOptions.BackdropType = value.Value;
}
}
}
public NameValue<ElementTheme>? SelectedElementTheme
{
get => selectedElementTheme ??= AppOptions.LazyElementThemes.Value.Single(t => t.Value == AppOptions.ElementTheme);
set
{
if (SetProperty(ref selectedElementTheme, value) && value is not null)
{
AppOptions.ElementTheme = value.Value;
}
}
}
public NameValue<BackgroundImageType>? SelectedBackgroundImageType
{
get => selectedBackgroundImageType ??= AppOptions.BackgroundImageTypes.Single(t => t.Value == AppOptions.BackgroundImageType);
set
{
if (SetProperty(ref selectedBackgroundImageType, value) && value is not null)
{
AppOptions.BackgroundImageType = value.Value;
}
}
}
}

View File

@@ -0,0 +1,109 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.User;
using Snap.Hutao.UI.Xaml.View.Dialog;
namespace Snap.Hutao.ViewModel.Setting;
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class SettingDangerousFeatureViewModel : Abstraction.ViewModel
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly RuntimeOptions runtimeOptions;
private readonly LaunchOptions launchOptions;
private readonly IUserService userService;
public RuntimeOptions RuntimeOptions { get => runtimeOptions; }
public LaunchOptions LaunchOptions { get => launchOptions; }
public bool IsAllocConsoleDebugModeEnabled
{
get => LocalSetting.Get(SettingKeys.IsAllocConsoleDebugModeEnabled, ConsoleWindowLifeTime.DebugModeEnabled);
set
{
if (IsViewDisposed)
{
return;
}
ConfirmSetIsAllocConsoleDebugModeEnabledAsync(value).SafeForget();
async ValueTask ConfirmSetIsAllocConsoleDebugModeEnabledAsync(bool value)
{
if (value)
{
ReconfirmDialog dialog = await contentDialogFactory.CreateInstanceAsync<ReconfirmDialog>().ConfigureAwait(false);
if (await dialog.ConfirmAsync(SH.ViewSettingAllocConsoleHeader).ConfigureAwait(true))
{
LocalSetting.Set(SettingKeys.IsAllocConsoleDebugModeEnabled, true);
OnPropertyChanged(nameof(IsAllocConsoleDebugModeEnabled));
return;
}
}
LocalSetting.Set(SettingKeys.IsAllocConsoleDebugModeEnabled, false);
OnPropertyChanged(nameof(IsAllocConsoleDebugModeEnabled));
}
}
}
public bool IsAdvancedLaunchOptionsEnabled
{
get => launchOptions.IsAdvancedLaunchOptionsEnabled;
set
{
if (IsViewDisposed)
{
return;
}
ConfirmSetIsAdvancedLaunchOptionsEnabledAsync(value).SafeForget();
async ValueTask ConfirmSetIsAdvancedLaunchOptionsEnabledAsync(bool value)
{
if (value)
{
ReconfirmDialog dialog = await contentDialogFactory.CreateInstanceAsync<ReconfirmDialog>().ConfigureAwait(false);
if (await dialog.ConfirmAsync(SH.ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader).ConfigureAwait(true))
{
launchOptions.IsAdvancedLaunchOptionsEnabled = true;
OnPropertyChanged(nameof(IsAdvancedLaunchOptionsEnabled));
return;
}
}
launchOptions.IsAdvancedLaunchOptionsEnabled = false;
OnPropertyChanged(nameof(IsAdvancedLaunchOptionsEnabled));
}
}
}
[Command("DeleteUsersCommand")]
private async Task DeleteUsersAsync()
{
if (userService is not IUserServiceUnsafe @unsafe)
{
return;
}
ContentDialogResult result = await contentDialogFactory
.CreateForConfirmCancelAsync(SH.ViewDialogSettingDeleteUserDataTitle, SH.ViewDialogSettingDeleteUserDataContent)
.ConfigureAwait(false);
if (result is ContentDialogResult.Primary)
{
await @unsafe.UnsafeRemoveAllUsersAsync().ConfigureAwait(false);
AppInstance.Restart(string.Empty);
}
}
}

View File

@@ -8,13 +8,13 @@ using Windows.System;
namespace Snap.Hutao.ViewModel.Setting;
internal sealed partial class FolderViewModel : ObservableObject
internal sealed partial class SettingFolderViewModel : ObservableObject
{
private readonly ITaskContext taskContext;
private readonly string folder;
private string? size;
public FolderViewModel(ITaskContext taskContext, string folder)
public SettingFolderViewModel(ITaskContext taskContext, string folder)
{
this.taskContext = taskContext;
this.folder = folder;

View File

@@ -0,0 +1,116 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.IO;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Picker;
using Snap.Hutao.Model.InterChange.GachaLog;
using Snap.Hutao.Service;
using Snap.Hutao.Service.GachaLog;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.UIGF;
using Snap.Hutao.UI.Xaml.View.Dialog;
namespace Snap.Hutao.ViewModel.Setting;
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class SettingGachaLogViewModel : Abstraction.ViewModel
{
private readonly IFileSystemPickerInteraction fileSystemPickerInteraction;
private readonly IContentDialogFactory contentDialogFactory;
private readonly IGachaLogDbService gachaLogDbService;
private readonly JsonSerializerOptions jsonOptions;
private readonly IInfoBarService infoBarService;
private readonly IUIGFService uigfService;
private readonly AppOptions appOptions;
public AppOptions AppOptions { get => appOptions; }
[Command("ImportUIGFJsonCommand")]
private async Task ImportUIGFJsonAsync()
{
(bool isOk, ValueFile file) = fileSystemPickerInteraction.PickFile(
SH.ViewModelGachaUIGFImportPickerTitile,
[(SH.ViewModelGachaLogExportFileType, "*.json")]);
if (!isOk)
{
return;
}
ValueResult<bool, UIGF?> result = await file.DeserializeFromJsonAsync<UIGF>(jsonOptions).ConfigureAwait(false);
if (!result.TryGetValue(out UIGF? uigf))
{
infoBarService.Error(SH.ViewModelImportWarningTitle, SH.ViewModelImportWarningMessage);
return;
}
if (uigf.Hk4e.IsNullOrEmpty())
{
infoBarService.Warning(SH.ViewModelUIGFImportNoHk4eEntry);
return;
}
if (uigf.Hk4e.Select(entry => entry.Uid).ToHashSet().Count != uigf.Hk4e.Count)
{
infoBarService.Warning(SH.ViewModelUIGFImportDuplicatedHk4eEntry);
return;
}
UIGFImportDialog importDialog = await contentDialogFactory.CreateInstanceAsync<UIGFImportDialog>(uigf).ConfigureAwait(false);
(bool isOk2, HashSet<string> uids) = await importDialog.GetSelectedUidsAsync().ConfigureAwait(false);
if (!isOk2)
{
return;
}
if (uids.IsNullOrEmpty())
{
infoBarService.Warning(SH.ViewModelUIGFImportNoSelectedEntry);
return;
}
UIGFImportOptions options = new()
{
UIGF = uigf,
GachaArchiveUids = uids,
};
try
{
await uigfService.ImportAsync(options).ConfigureAwait(false);
infoBarService.Success(SH.ViewModelUIGFImportSuccess);
}
catch (Exception ex)
{
infoBarService.Error(ex, SH.ViewModelUIGFImportSuccess);
}
}
[Command("ExportUIGFJsonCommand")]
private async Task ExportUIGFJsonAsync()
{
(bool isOk, ValueFile file) = fileSystemPickerInteraction.SaveFile(
SH.ViewModelGachaLogUIGFExportPickerTitle,
$"Snap Hutao UIGF.json",
[(SH.ViewModelGachaLogExportFileType, "*.json")]);
if (!isOk)
{
return;
}
LegacyUIGF uigf = await gachaLogService.ExportToUIGFAsync(Archives.CurrentItem).ConfigureAwait(false);
if (await file.SerializeToJsonAsync(uigf, options).ConfigureAwait(false))
{
infoBarService.Success(SH.ViewModelExportSuccessTitle, SH.ViewModelExportSuccessMessage);
}
else
{
infoBarService.Warning(SH.ViewModelExportWarningTitle, SH.ViewModelExportWarningMessage);
}
}
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.GachaLog.QueryProvider;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Notification;
using System.IO;
namespace Snap.Hutao.ViewModel.Setting;
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class SettingGameViewModel : Abstraction.ViewModel
{
private readonly IInfoBarService infoBarService;
private readonly LaunchOptions launchOptions;
[Command("DeleteGameWebCacheCommand")]
private void DeleteGameWebCache()
{
string gamePath = launchOptions.GamePath;
if (string.IsNullOrEmpty(gamePath))
{
return;
}
string cacheFilePath = GachaLogQueryWebCacheProvider.GetCacheFile(gamePath);
string? cacheFolder = Path.GetDirectoryName(cacheFilePath);
if (Directory.Exists(cacheFolder))
{
try
{
Directory.Delete(cacheFolder, true);
}
catch (UnauthorizedAccessException)
{
infoBarService.Warning(SH.ViewModelSettingClearWebCacheFail);
return;
}
infoBarService.Success(SH.ViewModelSettingClearWebCacheSuccess);
}
else
{
infoBarService.Warning(SH.FormatViewModelSettingClearWebCachePathInvalid(cacheFolder));
}
}
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Service;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.UI.Xaml.View.Dialog;
namespace Snap.Hutao.ViewModel.Setting;
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class SettingGeetestViewModel : Abstraction.ViewModel
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
private readonly AppOptions appOptions;
[Command("ConfigureGeetestUrlCommand")]
private async Task ConfigureGeetestUrlAsync()
{
GeetestCustomUrlDialog dialog = await contentDialogFactory.CreateInstanceAsync<GeetestCustomUrlDialog>().ConfigureAwait(false);
(bool isOk, string template) = await dialog.GetUrlAsync().ConfigureAwait(false);
if (!isOk)
{
return;
}
await taskContext.SwitchToMainThreadAsync();
appOptions.GeetestCustomCompositeUrl = template;
infoBarService.Success(SH.ViewModelSettingGeetestCustomUrlSucceed);
}
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model;
using Snap.Hutao.Service;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.ViewModel.Setting;
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class SettingHomeViewModel : Abstraction.ViewModel
{
private readonly HomeCardOptions homeCardOptions = new();
private readonly AppOptions appOptions;
private NameValue<Region>? selectedRegion;
public HomeCardOptions HomeCardOptions { get => homeCardOptions; }
public AppOptions AppOptions { get => appOptions; }
public NameValue<Region>? SelectedRegion
{
get => selectedRegion ??= AppOptions.GetCurrentRegionForSelectionOrDefault();
set
{
if (SetProperty(ref selectedRegion, value) && value is not null)
{
AppOptions.Region = value.Value;
}
}
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.UI.Input.HotKey;
namespace Snap.Hutao.ViewModel.Setting;
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class SettingHotKeyViewModel : Abstraction.ViewModel
{
private readonly RuntimeOptions runtimeOptions;
private readonly HotKeyOptions hotKeyOptions;
public RuntimeOptions RuntimeOptions { get => runtimeOptions; }
public HotKeyOptions HotKeyOptions { get => hotKeyOptions; }
}

View File

@@ -0,0 +1,97 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Picker;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.UI.Xaml.Control;
using Snap.Hutao.ViewModel.Guide;
using System.IO;
using Windows.System;
namespace Snap.Hutao.ViewModel.Setting;
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class SettingStorageViewModel : Abstraction.ViewModel
{
private readonly IFileSystemPickerInteraction fileSystemPickerInteraction;
private readonly IContentDialogFactory contentDialogFactory;
private readonly IInfoBarService infoBarService;
private readonly RuntimeOptions runtimeOptions;
private readonly ITaskContext taskContext;
private SettingFolderViewModel? cacheFolderView;
private SettingFolderViewModel? dataFolderView;
public SettingFolderViewModel? CacheFolderView { get => cacheFolderView; set => SetProperty(ref cacheFolderView, value); }
public SettingFolderViewModel? DataFolderView { get => dataFolderView; set => SetProperty(ref dataFolderView, value); }
[Command("OpenBackgroundImageFolderCommand")]
private async Task OpenBackgroundImageFolderAsync()
{
await Launcher.LaunchFolderPathAsync(runtimeOptions.GetDataFolderBackgroundFolder());
}
[Command("SetDataFolderCommand")]
private void SetDataFolder()
{
if (fileSystemPickerInteraction.PickFolder().TryGetValue(out string folder))
{
LocalSetting.Set(SettingKeys.DataFolderPath, folder);
infoBarService.Success(SH.ViewModelSettingSetDataFolderSuccess);
}
}
[Command("DeleteServerCacheFolderCommand")]
private async Task DeleteServerCacheFolderAsync()
{
ContentDialogResult result = await contentDialogFactory.CreateForConfirmCancelAsync(
SH.ViewModelSettingDeleteServerCacheFolderTitle,
SH.ViewModelSettingDeleteServerCacheFolderContent)
.ConfigureAwait(false);
if (result is not ContentDialogResult.Primary)
{
return;
}
string cacheFolder = runtimeOptions.GetDataFolderServerCacheFolder();
if (Directory.Exists(cacheFolder))
{
Directory.Delete(cacheFolder, true);
}
if (DataFolderView is not null)
{
await DataFolderView.SetFolderSizeAsync().ConfigureAwait(false);
}
infoBarService.Success(SH.ViewModelSettingActionComplete);
}
[Command("ResetStaticResourceCommand")]
private async Task ResetStaticResource()
{
ContentDialog dialog = await contentDialogFactory
.CreateForIndeterminateProgressAsync(SH.ViewModelSettingResetStaticResourceProgress)
.ConfigureAwait(false);
await using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{
await taskContext.SwitchToBackgroundAsync();
StaticResource.FailAll();
Directory.Delete(Path.Combine(runtimeOptions.LocalCache, nameof(ImageCache)), true);
UnsafeLocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, GuideState.StaticResourceBegin);
}
// TODO: prompt user that restart will be non-elevated
AppInstance.Restart(string.Empty);
}
}

View File

@@ -1,369 +1,68 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Shell;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Picker;
using Snap.Hutao.Model;
using Snap.Hutao.Service;
using Snap.Hutao.Service.BackgroundImage;
using Snap.Hutao.Service.GachaLog.QueryProvider;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.UI.Input.HotKey;
using Snap.Hutao.UI.Xaml.Control;
using Snap.Hutao.UI.Xaml.Media.Backdrop;
using Snap.Hutao.UI.Xaml.View.Dialog;
using Snap.Hutao.UI.Xaml.View.Page;
using Snap.Hutao.ViewModel.Guide;
using Snap.Hutao.Web.Hoyolab;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using Windows.System;
namespace Snap.Hutao.ViewModel.Setting;
/// <summary>
/// 设置视图模型
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class SettingViewModel : Abstraction.ViewModel
{
private readonly HomeCardOptions homeCardOptions = new();
private readonly IFileSystemPickerInteraction fileSystemPickerInteraction;
private readonly SettingDangerousFeatureViewModel dangerousFeatureViewModel;
private readonly SettingAppearanceViewModel appearanceViewModel;
private readonly HutaoPassportViewModel hutaoPassportViewModel;
private readonly BackgroundImageOptions backgroundImageOptions;
private readonly IContentDialogFactory contentDialogFactory;
private readonly INavigationService navigationService;
private readonly SettingGachaLogViewModel gachaLogViewModel;
private readonly SettingGeetestViewModel geetestViewModel;
private readonly SettingStorageViewModel storageViewModel;
private readonly SettingHotKeyViewModel hotKeyViewModel;
private readonly SettingHomeViewModel homeViewModel;
private readonly SettingGameViewModel gameViewModel;
private readonly IShellLinkInterop shellLinkInterop;
private readonly HutaoUserOptions hutaoUserOptions;
private readonly IInfoBarService infoBarService;
private readonly CultureOptions cultureOptions;
private readonly RuntimeOptions runtimeOptions;
private readonly LaunchOptions launchOptions;
private readonly HotKeyOptions hotKeyOptions;
private readonly IUserService userService;
private readonly ITaskContext taskContext;
private readonly AppOptions appOptions;
private readonly IMessenger messenger;
private NameValue<BackdropType>? selectedBackdropType;
private NameValue<ElementTheme>? selectedElementTheme;
private NameValue<BackgroundImageType>? selectedBackgroundImageType;
private NameValue<CultureInfo>? selectedCulture;
private NameValue<Region>? selectedRegion;
private FolderViewModel? cacheFolderView;
private FolderViewModel? dataFolderView;
public AppOptions AppOptions { get => appOptions; }
public CultureOptions CultureOptions { get => cultureOptions; }
public RuntimeOptions RuntimeOptions { get => runtimeOptions; }
public HutaoUserOptions UserOptions { get => hutaoUserOptions; }
public HomeCardOptions HomeCardOptions { get => homeCardOptions; }
public HotKeyOptions HotKeyOptions { get => hotKeyOptions; }
public LaunchOptions LaunchOptions { get => launchOptions; }
public BackgroundImageOptions BackgroundImageOptions { get => backgroundImageOptions; }
public HutaoPassportViewModel Passport { get => hutaoPassportViewModel; }
public NameValue<BackdropType>? SelectedBackdropType
{
get => selectedBackdropType ??= AppOptions.BackdropTypes.Single(t => t.Value == AppOptions.BackdropType);
set
{
if (SetProperty(ref selectedBackdropType, value) && value is not null)
{
AppOptions.BackdropType = value.Value;
}
}
}
public SettingGeetestViewModel Geetest { get => geetestViewModel; }
public NameValue<ElementTheme>? SelectedElementTheme
{
get => selectedElementTheme ??= AppOptions.LazyElementThemes.Value.Single(t => t.Value == AppOptions.ElementTheme);
set
{
if (SetProperty(ref selectedElementTheme, value) && value is not null)
{
AppOptions.ElementTheme = value.Value;
}
}
}
public SettingAppearanceViewModel Appearance { get => appearanceViewModel; }
public NameValue<BackgroundImageType>? SelectedBackgroundImageType
{
get => selectedBackgroundImageType ??= AppOptions.BackgroundImageTypes.Single(t => t.Value == AppOptions.BackgroundImageType);
set
{
if (SetProperty(ref selectedBackgroundImageType, value) && value is not null)
{
AppOptions.BackgroundImageType = value.Value;
}
}
}
public SettingStorageViewModel Storage { get => storageViewModel; }
public NameValue<CultureInfo>? SelectedCulture
{
get => selectedCulture ??= CultureOptions.GetCurrentCultureForSelectionOrDefault();
set
{
if (SetProperty(ref selectedCulture, value) && value is not null)
{
CultureOptions.CurrentCulture = value.Value;
AppInstance.Restart(string.Empty);
}
}
}
public SettingHotKeyViewModel HotKey { get => hotKeyViewModel; }
public NameValue<Region>? SelectedRegion
{
get => selectedRegion ??= AppOptions.GetCurrentRegionForSelectionOrDefault();
set
{
if (SetProperty(ref selectedRegion, value) && value is not null)
{
AppOptions.Region = value.Value;
}
}
}
public SettingHomeViewModel Home { get => homeViewModel; }
public FolderViewModel? CacheFolderView { get => cacheFolderView; set => SetProperty(ref cacheFolderView, value); }
public SettingGameViewModel Game { get => gameViewModel; }
public FolderViewModel? DataFolderView { get => dataFolderView; set => SetProperty(ref dataFolderView, value); }
public SettingGachaLogViewModel GachaLog { get => gachaLogViewModel; }
public bool IsAllocConsoleDebugModeEnabled
{
get => LocalSetting.Get(SettingKeys.IsAllocConsoleDebugModeEnabled, ConsoleWindowLifeTime.DebugModeEnabled);
set
{
if (IsViewDisposed)
{
return;
}
ConfirmSetIsAllocConsoleDebugModeEnabledAsync(value).SafeForget();
async ValueTask ConfirmSetIsAllocConsoleDebugModeEnabledAsync(bool value)
{
if (value)
{
ReconfirmDialog dialog = await contentDialogFactory.CreateInstanceAsync<ReconfirmDialog>().ConfigureAwait(false);
if (await dialog.ConfirmAsync(SH.ViewSettingAllocConsoleHeader).ConfigureAwait(true))
{
LocalSetting.Set(SettingKeys.IsAllocConsoleDebugModeEnabled, true);
OnPropertyChanged(nameof(IsAllocConsoleDebugModeEnabled));
return;
}
}
LocalSetting.Set(SettingKeys.IsAllocConsoleDebugModeEnabled, false);
OnPropertyChanged(nameof(IsAllocConsoleDebugModeEnabled));
}
}
}
public bool IsAdvancedLaunchOptionsEnabled
{
get => launchOptions.IsAdvancedLaunchOptionsEnabled;
set
{
if (IsViewDisposed)
{
return;
}
ConfirmSetIsAdvancedLaunchOptionsEnabledAsync(value).SafeForget();
async ValueTask ConfirmSetIsAdvancedLaunchOptionsEnabledAsync(bool value)
{
if (value)
{
ReconfirmDialog dialog = await contentDialogFactory.CreateInstanceAsync<ReconfirmDialog>().ConfigureAwait(false);
if (await dialog.ConfirmAsync(SH.ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader).ConfigureAwait(true))
{
launchOptions.IsAdvancedLaunchOptionsEnabled = true;
OnPropertyChanged(nameof(IsAdvancedLaunchOptionsEnabled));
return;
}
}
launchOptions.IsAdvancedLaunchOptionsEnabled = false;
OnPropertyChanged(nameof(IsAdvancedLaunchOptionsEnabled));
}
}
}
public SettingDangerousFeatureViewModel DangerousFeature { get => dangerousFeatureViewModel; }
protected override ValueTask<bool> InitializeOverrideAsync()
{
CacheFolderView = new(taskContext, runtimeOptions.LocalCache);
DataFolderView = new(taskContext, runtimeOptions.DataFolder);
Storage.CacheFolderView = new(taskContext, runtimeOptions.LocalCache);
Storage.DataFolderView = new(taskContext, runtimeOptions.DataFolder);
return ValueTask.FromResult(true);
}
[Command("StoreReviewCommand")]
private static async Task StoreReviewAsync()
protected override void UninitializeOverride()
{
await Launcher.LaunchUriAsync(new("ms-windows-store://review/?ProductId=9PH4NXJ2JN52"));
}
[Command("UpdateCheckCommand")]
private static async Task CheckUpdateAsync()
{
await Launcher.LaunchUriAsync(new("ms-windows-store://pdp/?productid=9PH4NXJ2JN52"));
}
[Command("ResetStaticResourceCommand")]
private async Task ResetStaticResource()
{
ContentDialog dialog = await contentDialogFactory
.CreateForIndeterminateProgressAsync(SH.ViewModelSettingResetStaticResourceProgress)
.ConfigureAwait(false);
await using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{
await taskContext.SwitchToBackgroundAsync();
StaticResource.FailAll();
Directory.Delete(Path.Combine(runtimeOptions.LocalCache, nameof(ImageCache)), true);
UnsafeLocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, GuideState.StaticResourceBegin);
}
AppInstance.Restart(string.Empty);
}
[Command("DeleteGameWebCacheCommand")]
private void DeleteGameWebCache()
{
string gamePath = launchOptions.GamePath;
if (!string.IsNullOrEmpty(gamePath))
{
string cacheFilePath = GachaLogQueryWebCacheProvider.GetCacheFile(gamePath);
string? cacheFolder = Path.GetDirectoryName(cacheFilePath);
if (Directory.Exists(cacheFolder))
{
try
{
Directory.Delete(cacheFolder, true);
}
catch (UnauthorizedAccessException)
{
infoBarService.Warning(SH.ViewModelSettingClearWebCacheFail);
return;
}
infoBarService.Success(SH.ViewModelSettingClearWebCacheSuccess);
}
else
{
infoBarService.Warning(SH.FormatViewModelSettingClearWebCachePathInvalid(cacheFolder));
}
}
}
[Command("OpenTestPageCommand")]
private async Task OpenTestPageAsync()
{
await navigationService
.NavigateAsync<TestPage>(INavigationAwaiter.Default)
.ConfigureAwait(false);
}
[Command("SetDataFolderCommand")]
private void SetDataFolder()
{
(bool isOk, string folder) = fileSystemPickerInteraction.PickFolder();
if (isOk)
{
LocalSetting.Set(SettingKeys.DataFolderPath, folder);
infoBarService.Success(SH.ViewModelSettingSetDataFolderSuccess);
}
}
[Command("DeleteServerCacheFolderCommand")]
private async Task DeleteServerCacheFolderAsync()
{
ContentDialogResult result = await contentDialogFactory.CreateForConfirmCancelAsync(
SH.ViewModelSettingDeleteServerCacheFolderTitle,
SH.ViewModelSettingDeleteServerCacheFolderContent)
.ConfigureAwait(false);
if (result is ContentDialogResult.Primary)
{
string cacheFolder = runtimeOptions.GetDataFolderServerCacheFolder();
if (Directory.Exists(cacheFolder))
{
Directory.Delete(cacheFolder, true);
}
if (DataFolderView is not null)
{
await DataFolderView.SetFolderSizeAsync().ConfigureAwait(false);
}
infoBarService.Success(SH.ViewModelSettingActionComplete);
}
}
[Command("OpenBackgroundImageFolderCommand")]
private async Task OpenBackgroundImageFolderAsync()
{
await Launcher.LaunchFolderPathAsync(runtimeOptions.GetDataFolderBackgroundFolder());
}
[Command("DeleteUsersCommand")]
private async Task DangerousDeleteUsersAsync()
{
if (userService is IUserServiceUnsafe @unsafe)
{
ContentDialogResult result = await contentDialogFactory
.CreateForConfirmCancelAsync(SH.ViewDialogSettingDeleteUserDataTitle, SH.ViewDialogSettingDeleteUserDataContent)
.ConfigureAwait(false);
if (result is ContentDialogResult.Primary)
{
await @unsafe.UnsafeRemoveAllUsersAsync().ConfigureAwait(false);
AppInstance.Restart(string.Empty);
}
}
}
[Command("ConfigureGeetestUrlCommand")]
private async Task ConfigureGeetestUrlAsync()
{
GeetestCustomUrlDialog dialog = await contentDialogFactory.CreateInstanceAsync<GeetestCustomUrlDialog>().ConfigureAwait(false);
(bool isOk, string template) = await dialog.GetUrlAsync().ConfigureAwait(false);
if (isOk)
{
await taskContext.SwitchToMainThreadAsync();
appOptions.GeetestCustomCompositeUrl = template;
infoBarService.Success(SH.ViewModelSettingGeetestCustomUrlSucceed);
}
Passport.IsViewDisposed = true;
Geetest.IsViewDisposed = true;
Appearance.IsViewDisposed = true;
Storage.IsViewDisposed = true;
HotKey.IsViewDisposed = true;
Home.IsViewDisposed = true;
Game.IsViewDisposed = true;
GachaLog.IsViewDisposed = true;
DangerousFeature.IsViewDisposed = true;
}
[Command("CreateDesktopShortcutCommand")]