mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
1 Commits
1.10.7
...
feat/sopho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97a0b0b4d7 |
@@ -3,12 +3,26 @@
|
||||
|
||||
namespace Snap.Hutao.Model.Calculable;
|
||||
|
||||
/// <summary>
|
||||
/// 可计算物品选项
|
||||
/// </summary>
|
||||
internal readonly struct CalculableOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色
|
||||
/// </summary>
|
||||
public readonly ICalculableAvatar? Avatar;
|
||||
|
||||
/// <summary>
|
||||
/// 武器
|
||||
/// </summary>
|
||||
public readonly ICalculableWeapon? Weapon;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的可计算物品选项
|
||||
/// </summary>
|
||||
/// <param name="avatar">角色</param>
|
||||
/// <param name="weapon">武器</param>
|
||||
public CalculableOptions(ICalculableAvatar? avatar, ICalculableWeapon? weapon)
|
||||
{
|
||||
Avatar = avatar;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<Identity
|
||||
Name="60568DGPStudio.SnapHutao"
|
||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||
Version="1.10.7.0" />
|
||||
Version="1.10.6.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Snap Hutao</DisplayName>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<Identity
|
||||
Name="60568DGPStudio.SnapHutaoDev"
|
||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||
Version="1.10.7.0" />
|
||||
Version="1.10.6.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Snap Hutao Dev</DisplayName>
|
||||
|
||||
@@ -1247,18 +1247,6 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>Add or update to current Enhancement Progression Plan</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>Always create new adopted target items</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>Save Method</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>Overwrite existing adopted items</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>Keep existing target items</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>Daily Commission Availability Notification</value>
|
||||
</data>
|
||||
@@ -1610,15 +1598,6 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>Operation not fully completed, added/updated {0}, skipped {1}</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>The selected level does not require ingredients</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>No plan has been created and selected</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>There is already a foster project for this item</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>Successfully added to current plan</value>
|
||||
</data>
|
||||
@@ -1839,10 +1818,10 @@
|
||||
<value>Export failed</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFExportSuccess" xml:space="preserve">
|
||||
<value>Exported successfully</value>
|
||||
<value>Export successfully</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFImportDuplicatedHk4eEntry" xml:space="preserve">
|
||||
<value>Imported UIGF files contain UID duplicated wish items</value>
|
||||
<value>Imported UIGF files contain UID duplicated prayers</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFImportError" xml:space="preserve">
|
||||
<value>Import failed</value>
|
||||
@@ -2868,7 +2847,7 @@
|
||||
<value>Import</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUnobtainedWishItemVisibleDescription" xml:space="preserve">
|
||||
<value>Display Undrawn Items in "Wish Export - Characters" and "Wish Export - Weapons"</value>
|
||||
<value>在「祈愿记录-角色」与「祈愿记录-武器」中显示未抽取到的祈愿物品</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUnobtainedWishItemVisibleHeader" xml:space="preserve">
|
||||
<value>Undrawn Wish Items</value>
|
||||
|
||||
@@ -1247,18 +1247,6 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>添加或更新到当前养成计划</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>每日委托上线提醒</value>
|
||||
</data>
|
||||
@@ -1610,15 +1598,6 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>操作未全部完成:添加/更新:{0} 个,跳过 {1} 个</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>已成功添加至当前养成计划</value>
|
||||
</data>
|
||||
|
||||
@@ -1247,18 +1247,6 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>Tambahkan atau perbarui ke Rencana Dev saat ini</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>Pemberitahuan Ketersediaan Komisi Harian</value>
|
||||
</data>
|
||||
@@ -1610,15 +1598,6 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>Operasi tidak sepenuhnya selesai, ditambahkan/diperbarui {0}, dilewati {1}</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>Berhasil ditambahkan ke rencana saat ini.</value>
|
||||
</data>
|
||||
|
||||
@@ -1247,18 +1247,6 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>現在の育成計画に追加または更新する</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>アイテムは常に新しいドリルを作成</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>次で保存</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>既存の育成目的アイテム</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>対象のアイテムを予約する</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>まだデイリー依頼を行っていません</value>
|
||||
</data>
|
||||
@@ -1610,15 +1598,6 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>操作の一部に失敗しました:追加/更新:{0}、スキップ{1}</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>選択したLvでは育成材料は必要ありません</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>まだ作成されておらず、養成計画を選択してください</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>そのアイテムは既に適用されているアイテムがあります</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>選択中の育成計画に正常に追加されました</value>
|
||||
</data>
|
||||
|
||||
@@ -1247,18 +1247,6 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>添加或更新到当前养成计划</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>일일 의뢰 가능 알림</value>
|
||||
</data>
|
||||
@@ -1610,15 +1598,6 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>操作未全部完成:添加/更新:{0} 个,跳过 {1} 个</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>현재 육성 계획 추가에 성공했습니다</value>
|
||||
</data>
|
||||
|
||||
@@ -1247,18 +1247,6 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>Adicionar ou atualizar ao planejamento atual</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>Notificação diária de disponibilidade de missão</value>
|
||||
</data>
|
||||
@@ -1610,15 +1598,6 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>Operação não totalmente concluída, adicionado/atualizado {0}, pulado {1}</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>Adicionado com sucesso ao planejamento</value>
|
||||
</data>
|
||||
|
||||
@@ -1247,18 +1247,6 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>添加或更新到当前养成计划</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>每日委托上线提醒</value>
|
||||
</data>
|
||||
@@ -1610,15 +1598,6 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>操作未全部完成:添加 / 更新:{0} 个,跳过 {1} 个</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>已成功添加至当前养成计划</value>
|
||||
</data>
|
||||
|
||||
@@ -1247,18 +1247,6 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>添加或更新到当前养成计划</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>每日委托上线提醒</value>
|
||||
</data>
|
||||
@@ -1610,15 +1598,6 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>操作未全部完成:添加/更新:{0} 个,跳过 {1} 个</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>已成功添加至当前养成计划</value>
|
||||
</data>
|
||||
|
||||
@@ -1247,18 +1247,6 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>添加或更新到当前养成计划</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>每日委托上线提醒</value>
|
||||
</data>
|
||||
@@ -1610,15 +1598,6 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>操作未全部完成:添加/更新:{0} 个,跳过 {1} 个</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>已成功添加至当前养成计划</value>
|
||||
</data>
|
||||
|
||||
@@ -557,10 +557,10 @@
|
||||
<value>必須登入 米游社/ HoYoLAB 並選擇一個用戶與角色</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceDeleteEntrySucceed" xml:space="preserve">
|
||||
<value>刪除了 UID:{0} 的 {1} 筆祈願紀錄</value>
|
||||
<value>刪除了 UID:{0} 的 {1} 筆祈願記錄</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInsufficientRecordSlot" xml:space="preserve">
|
||||
<value>胡桃雲儲存的祈願紀錄存檔數已達當前帳號上限</value>
|
||||
<value>胡桃雲儲存的祈願記錄存檔數已達當前帳號上限</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInsufficientTime" xml:space="preserve">
|
||||
<value>未開通祈願紀錄上傳服務或已到期</value>
|
||||
@@ -572,7 +572,7 @@
|
||||
<value>數據異常,無法儲存至雲端,請勿跨帳號上傳或嘗試删除雲端數據後重試</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceUploadEntrySucceed" xml:space="preserve">
|
||||
<value>上傳了 UID:{0} 的 {1} 筆祈願紀錄,儲存了 {2} 筆</value>
|
||||
<value>上傳了 UID:{0} 的 {1} 筆祈願記錄,儲存了 {2} 筆</value>
|
||||
</data>
|
||||
<data name="ServerPassportLoginRequired" xml:space="preserve">
|
||||
<value>請先登入或註冊胡桃帳號</value>
|
||||
@@ -623,40 +623,40 @@
|
||||
<value>驗證請求過快,請 1 分鐘後再試</value>
|
||||
</data>
|
||||
<data name="ServerRecordBannedUid" xml:space="preserve">
|
||||
<value>上傳深淵紀錄失敗,當前 UID 已被胡桃數據庫封禁</value>
|
||||
<value>上傳深淵記錄失敗,當前 UID 已被胡桃數據庫封禁</value>
|
||||
</data>
|
||||
<data name="ServerRecordComputingStatistics" xml:space="preserve">
|
||||
<value>上傳深淵紀錄失敗,正在計算統計數據</value>
|
||||
<value>上傳深淵記錄失敗,正在計算統計數據</value>
|
||||
</data>
|
||||
<data name="ServerRecordComputingStatistics2" xml:space="preserve">
|
||||
<value>獲取數據失敗,正在計算統計數據</value>
|
||||
</data>
|
||||
<data name="ServerRecordInternalException" xml:space="preserve">
|
||||
<value>上傳深淵紀錄失敗,伺服器異常,請盡快聯繫開發者解決</value>
|
||||
<value>上傳深淵記錄失敗,伺服器異常,請盡快聯繫開發者解決</value>
|
||||
</data>
|
||||
<data name="ServerRecordInvalidData" xml:space="preserve">
|
||||
<value>上傳深淵紀錄失敗,存在無效的數據</value>
|
||||
<value>上傳深淵記錄失敗,存在無效的數據</value>
|
||||
</data>
|
||||
<data name="ServerRecordInvalidUid" xml:space="preserve">
|
||||
<value>無效的 UID</value>
|
||||
</data>
|
||||
<data name="ServerRecordNotCurrentSchedule" xml:space="preserve">
|
||||
<value>上傳深淵紀錄失敗,不是本期數據</value>
|
||||
<value>上傳深淵記錄失敗,不是本期數據</value>
|
||||
</data>
|
||||
<data name="ServerRecordPreviousRequestNotCompleted" xml:space="preserve">
|
||||
<value>上傳深淵紀錄失敗,當前 UID 的紀錄仍在處理中,請勿重複操作</value>
|
||||
<value>上傳深淵記錄失敗,當前 UID 的紀錄仍在處理中,請勿重複操作</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessAndGachaLogServiceTimeExtended" xml:space="preserve">
|
||||
<value>上傳深淵紀錄成功,獲贈祈願紀錄上傳服務時長</value>
|
||||
<value>上傳深淵記錄成功,獲贈祈願記錄上傳服務時長</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessButNoPassport" xml:space="preserve">
|
||||
<value>上傳深淵紀錄成功,但未登入胡桃通行證,無法獲贈祈願紀錄上傳服務時長</value>
|
||||
<value>上傳深淵記錄成功,但未登入胡桃通行證,無法獲贈祈願記錄上傳服務時長</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessButNoSuchUser" xml:space="preserve">
|
||||
<value>上傳深淵紀錄成功,但無法找到用戶,無法獲贈祈願紀錄上傳服務時長</value>
|
||||
<value>上傳深淵記錄成功,但無法找到用戶,無法獲贈祈願記錄上傳服務時長</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessButNotFirstTimeAtCurrentSchedule" xml:space="preserve">
|
||||
<value>上傳深淵紀錄成功,但不是本期首次提交,無法獲贈祈願紀錄上傳服務時長</value>
|
||||
<value>上傳深淵記錄成功,但不是本期首次提交,無法獲贈祈願記錄上傳服務時長</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementImportResultFormat" xml:space="preserve">
|
||||
<value>新增:{0} 個成就 | 更新:{1} 個成就 | 删除:{2} 個成就</value>
|
||||
@@ -878,10 +878,10 @@
|
||||
<value>由 {0} 啟動</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogArchiveCollectionUserdataCorruptedMessage" xml:space="preserve">
|
||||
<value>無法獲取祈願紀錄:{0}</value>
|
||||
<value>無法獲取祈願記錄:{0}</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogEndIdUserdataCorruptedMessage" xml:space="preserve">
|
||||
<value>無法獲取祈願紀錄 End Id</value>
|
||||
<value>無法獲取祈願記錄 End Id</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogFactoryAvatarWishName" xml:space="preserve">
|
||||
<value>角色活動</value>
|
||||
@@ -899,7 +899,7 @@
|
||||
<value>獲取雲端祈願紀錄失敗</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogHutaoCloudServiceNotAllowed" xml:space="preserve">
|
||||
<value>祈願紀錄上傳服務不可用</value>
|
||||
<value>祈願記錄上傳服務不可用</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogUIGFImportItemInvalidFormat" xml:space="preserve">
|
||||
<value>數據包含異常物品, Id:{0}</value>
|
||||
@@ -921,7 +921,7 @@
|
||||
<value>提供的 URL 無效</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogUrlProviderStokenUnsupported" xml:space="preserve">
|
||||
<value>HoYoLAB 帳號不支持使用 SToken 重新整理祈願紀錄</value>
|
||||
<value>HoYoLAB 帳號不支持使用 SToken 重新整理祈願記錄</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale" xml:space="preserve">
|
||||
<value>URL 中的語言:{0} 與胡桃的語言:{1} 不對應,請切換到對應語言重試</value>
|
||||
@@ -1247,18 +1247,6 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>添加或更新到當前養成計劃</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>每日委託上線提醒</value>
|
||||
</data>
|
||||
@@ -1287,10 +1275,10 @@
|
||||
<value>即時便箋 Webhook URL</value>
|
||||
</data>
|
||||
<data name="ViewDialogExportUIGFSubtitle" xml:space="preserve">
|
||||
<value>選擇要匯出紀錄的 UID</value>
|
||||
<value>选择要导出记录的 UID</value>
|
||||
</data>
|
||||
<data name="ViewDialogExportUIGFTitle" xml:space="preserve">
|
||||
<value>匯出 UIGF 文件</value>
|
||||
<value>导出 UIGF 文件</value>
|
||||
</data>
|
||||
<data name="ViewDialogFeedbackEnableLoopbackContent" xml:space="preserve">
|
||||
<value>解除限制後需使用其他工具恢復限制</value>
|
||||
@@ -1299,10 +1287,10 @@
|
||||
<value>是否解除 Loopback 限制</value>
|
||||
</data>
|
||||
<data name="ViewDialogGachaLogImportTitle" xml:space="preserve">
|
||||
<value>匯入祈願紀錄</value>
|
||||
<value>匯入祈願記錄</value>
|
||||
</data>
|
||||
<data name="ViewDialogGachaLogRefreshProgressAuthkeyTimeout" xml:space="preserve">
|
||||
<value>祈願紀錄 URL 已失效,請重新獲取</value>
|
||||
<value>祈願記錄 URL 已失效,請重新獲取</value>
|
||||
</data>
|
||||
<data name="ViewDialogGachaLogRefreshProgressDescription" xml:space="preserve">
|
||||
<value>正在獲取 {0}</value>
|
||||
@@ -1314,7 +1302,7 @@
|
||||
<value>請輸入 URL</value>
|
||||
</data>
|
||||
<data name="ViewDialogGachaLogUrlTitle" xml:space="preserve">
|
||||
<value>手動輸入祈願紀錄 URL</value>
|
||||
<value>手動輸入祈願記錄 URL</value>
|
||||
</data>
|
||||
<data name="ViewDialogGeetestCustomUrlCompositInputHint" xml:space="preserve">
|
||||
<value>請輸入請求接口的 URL 複合模板</value>
|
||||
@@ -1380,10 +1368,10 @@
|
||||
<value>UIGF 版本</value>
|
||||
</data>
|
||||
<data name="ViewDialogImportUIGFSubtitle" xml:space="preserve">
|
||||
<value>選擇要匯入紀錄的 UID</value>
|
||||
<value>选择要导入记录的 UID</value>
|
||||
</data>
|
||||
<data name="ViewDialogImportUIGFTitle" xml:space="preserve">
|
||||
<value>匯入 UIGF 檔案</value>
|
||||
<value>导入 UIGF 文件</value>
|
||||
</data>
|
||||
<data name="ViewDialogLaunchGameAccountInputPlaceholder" xml:space="preserve">
|
||||
<value>在此處輸入名稱</value>
|
||||
@@ -1455,7 +1443,7 @@
|
||||
<value>回饋中心</value>
|
||||
</data>
|
||||
<data name="ViewGachaLogHeader" xml:space="preserve">
|
||||
<value>祈願紀錄</value>
|
||||
<value>祈願記錄</value>
|
||||
</data>
|
||||
<data name="ViewGuideStaticResourceDownloadSize" xml:space="preserve">
|
||||
<value>預計下載大小:{0}</value>
|
||||
@@ -1610,15 +1598,6 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>操作未全部完成:添加/更新:{0} 個,跳過 {1} 個</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>已成功新增至目前養成計劃</value>
|
||||
</data>
|
||||
@@ -1842,19 +1821,19 @@
|
||||
<value>匯出成功</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFImportDuplicatedHk4eEntry" xml:space="preserve">
|
||||
<value>匯入的 UIGF 檔案中包含 UID 重複的祈願紀錄項</value>
|
||||
<value>导入的 UIGF 文件中包含 UID 重复的祈愿记录项</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFImportError" xml:space="preserve">
|
||||
<value>匯入失敗</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFImportNoHk4eEntry" xml:space="preserve">
|
||||
<value>匯入的 UIGF 檔案中不包含祈願數據</value>
|
||||
<value>导入的 UIGF 文件中不包含祈愿数据</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFImportNoSelectedEntry" xml:space="preserve">
|
||||
<value>請選擇至少一個 UID 以匯入數據</value>
|
||||
<value>请选择至少一个 UID 以导入数据</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFImportSuccess" xml:space="preserve">
|
||||
<value>匯入成功</value>
|
||||
<value>导入成功</value>
|
||||
</data>
|
||||
<data name="ViewModelUserAdded" xml:space="preserve">
|
||||
<value>用戶 [{0}] 新增成功</value>
|
||||
@@ -2214,7 +2193,7 @@
|
||||
<value>手動輸入 URL</value>
|
||||
</data>
|
||||
<data name="ViewPageGachaLogRefreshByManualInputDescription" xml:space="preserve">
|
||||
<value>使用由你提供的 URL 重新整理祈願紀錄</value>
|
||||
<value>使用由你提供的 URL 重新整理祈願記錄</value>
|
||||
</data>
|
||||
<data name="ViewPageGachaLogRefreshBySToken" xml:space="preserve">
|
||||
<value>SToken 重新整理</value>
|
||||
@@ -2307,7 +2286,7 @@
|
||||
<value>數據收集統計</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoDatabaseOverviewRecordTotal" xml:space="preserve">
|
||||
<value>上傳紀錄總數</value>
|
||||
<value>上傳記錄總數</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoDatabaseOverviewRefreshTime" xml:space="preserve">
|
||||
<value>數據重新整理時間</value>
|
||||
@@ -2319,16 +2298,16 @@
|
||||
<value>平均戰鬥次數</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoDatabaseOverviewSpiralAbyssFullStar" xml:space="preserve">
|
||||
<value>滿星深淵紀錄</value>
|
||||
<value>滿星深淵記錄</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoDatabaseOverviewSpiralAbyssPassed" xml:space="preserve">
|
||||
<value>通關深淵紀錄</value>
|
||||
<value>通關深淵記錄</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoDatabaseOverviewSpiralAbyssStarAverage" xml:space="preserve">
|
||||
<value>平均獲取淵星</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoDatabaseOverviewSpiralAbyssTotal" xml:space="preserve">
|
||||
<value>總計深淵紀錄</value>
|
||||
<value>總計深淵記錄</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoDatabaseOverviewTeamAppearance" xml:space="preserve">
|
||||
<value>隊伍出場</value>
|
||||
@@ -2631,7 +2610,7 @@
|
||||
<value>刪除遊戲内網頁快取</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingDeleteUserDescription" xml:space="preserve">
|
||||
<value>直接刪除用戶表的所有紀錄,用於修復特定的賬號衝突問題</value>
|
||||
<value>直接刪除用戶表的所有記錄,用於修復特定的賬號衝突問題</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingDeleteUserHeader" xml:space="preserve">
|
||||
<value>刪除所有用戶</value>
|
||||
@@ -2673,7 +2652,7 @@
|
||||
<value>前往反饋</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingGachaLogHeader" xml:space="preserve">
|
||||
<value>祈願紀錄</value>
|
||||
<value>祈願記錄</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingGameHeader" xml:space="preserve">
|
||||
<value>遊戲</value>
|
||||
@@ -2859,16 +2838,16 @@
|
||||
<value>匯出</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUIGFExportImportDescription" xml:space="preserve">
|
||||
<value>匯出/匯入 UIGF 4 檔案</value>
|
||||
<value>导出/导入 UIGF 4 文件</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUIGFExportImportHeader" xml:space="preserve">
|
||||
<value>數據遷移</value>
|
||||
<value>数据迁移</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUIGFImportContent" xml:space="preserve">
|
||||
<value>匯入</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUnobtainedWishItemVisibleDescription" xml:space="preserve">
|
||||
<value>在「祈願紀錄 - 角色」與「祈願紀錄 - 武器」中顯示未抽到的祈願物品</value>
|
||||
<value>在祈願記錄頁面角色與武器頁籤顯示未抽取到的祈願物品</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUnobtainedWishItemVisibleHeader" xml:space="preserve">
|
||||
<value>未抽取到的祈願物品</value>
|
||||
|
||||
@@ -8,10 +8,12 @@ using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.InterChange.Achievement;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.ViewModel.Achievement;
|
||||
using System.Collections.ObjectModel;
|
||||
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
|
||||
|
||||
namespace Snap.Hutao.Service.Achievement;
|
||||
|
||||
[HighQuality]
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Scoped, typeof(IAchievementService))]
|
||||
internal sealed partial class AchievementService : IAchievementService
|
||||
@@ -29,7 +31,9 @@ internal sealed partial class AchievementService : IAchievementService
|
||||
if (archives is null)
|
||||
{
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
archives = new(achievementDbService.GetAchievementArchiveCollection(), serviceProvider);
|
||||
ObservableCollection<AchievementArchive> source = achievementDbService.GetAchievementArchiveCollection();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
archives = new(source, serviceProvider);
|
||||
}
|
||||
|
||||
return archives;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
using Snap.Hutao.UI.Xaml.Data;
|
||||
using Snap.Hutao.ViewModel.AvatarProperty;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
@@ -18,6 +17,7 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
internal sealed partial class SummaryFactory : ISummaryFactory
|
||||
{
|
||||
private readonly IMetadataService metadataService;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Summary> CreateAsync(IEnumerable<Model.Entity.AvatarInfo> avatarInfos, CancellationToken token)
|
||||
@@ -37,9 +37,11 @@ internal sealed partial class SummaryFactory : ISummaryFactory
|
||||
|
||||
IList<AvatarView> views = [.. avatars];
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
return new()
|
||||
{
|
||||
Avatars = views.ToAdvancedCollectionView(),
|
||||
Avatars = new(views),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Cultivation;
|
||||
|
||||
internal enum ConsumptionSaveResultKind
|
||||
{
|
||||
NoItem,
|
||||
NoProject,
|
||||
Skipped,
|
||||
Added,
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Cultivation;
|
||||
|
||||
internal enum ConsumptionSaveStrategyKind
|
||||
{
|
||||
PreserveExisting,
|
||||
OverwriteExisting,
|
||||
CreateNewEntry,
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Snap.Hutao.Service.Inventory;
|
||||
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
using Snap.Hutao.ViewModel.Cultivation;
|
||||
using System.Collections.ObjectModel;
|
||||
using CalculateItem = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Item;
|
||||
using ModelItem = Snap.Hutao.Model.Item;
|
||||
|
||||
namespace Snap.Hutao.Service.Cultivation;
|
||||
@@ -114,54 +115,45 @@ internal sealed partial class CultivationService : ICultivationService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ConsumptionSaveResultKind> SaveConsumptionAsync(InputConsumption inputConsumption)
|
||||
public async ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<CalculateItem> items, LevelInformation levelInformation)
|
||||
{
|
||||
if (inputConsumption.Items.Count == 0)
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return ConsumptionSaveResultKind.NoItem;
|
||||
return true;
|
||||
}
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
if (Projects.CurrentItem is null)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
Projects.MoveCurrentTo(Projects.SourceCollection.SelectedOrDefault());
|
||||
if (Projects.CurrentItem is null)
|
||||
{
|
||||
return ConsumptionSaveResultKind.NoProject;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
|
||||
CultivateEntry? entry = default;
|
||||
|
||||
if (inputConsumption.Strategy is ConsumptionSaveStrategyKind.PreserveExisting or ConsumptionSaveStrategyKind.OverwriteExisting)
|
||||
{
|
||||
entry = cultivationDbService.GetCultivateEntryByProjectIdAndItemId(Projects.CurrentItem.InnerId, inputConsumption.ItemId);
|
||||
|
||||
if (inputConsumption.Strategy is ConsumptionSaveStrategyKind.PreserveExisting && entry is not null)
|
||||
{
|
||||
return ConsumptionSaveResultKind.Skipped;
|
||||
}
|
||||
}
|
||||
CultivateEntry? entry = type is CultivateType.AvatarAndSkill
|
||||
? cultivationDbService.GetCultivateEntryByProjectIdAndItemId(Projects.CurrentItem.InnerId, itemId)
|
||||
: default;
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
entry = CultivateEntry.From(Projects.CurrentItem.InnerId, inputConsumption.Type, inputConsumption.ItemId);
|
||||
entry = CultivateEntry.From(Projects.CurrentItem.InnerId, type, itemId);
|
||||
cultivationDbService.AddCultivateEntry(entry);
|
||||
}
|
||||
|
||||
Guid entryId = entry.InnerId;
|
||||
|
||||
cultivationDbService.RemoveLevelInformationByEntryId(entryId);
|
||||
CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entryId, inputConsumption.Type, inputConsumption.LevelInformation);
|
||||
CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entryId, type, levelInformation);
|
||||
cultivationDbService.AddLevelInformation(entryLevelInformation);
|
||||
|
||||
cultivationDbService.RemoveCultivateItemRangeByEntryId(entryId);
|
||||
IEnumerable<CultivateItem> toAdd = inputConsumption.Items.Select(item => CultivateItem.From(entryId, item));
|
||||
IEnumerable<CultivateItem> toAdd = items.Select(item => CultivateItem.From(entryId, item));
|
||||
cultivationDbService.AddCultivateItemRange(toAdd);
|
||||
|
||||
return ConsumptionSaveResultKind.Added;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -3,13 +3,22 @@
|
||||
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using Snap.Hutao.ViewModel.Cultivation;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Snap.Hutao.Service.Cultivation;
|
||||
|
||||
/// <summary>
|
||||
/// 养成计算服务
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal interface ICultivationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取用于绑定的项目集合
|
||||
/// </summary>
|
||||
AdvancedDbCollectionView<CultivateProject> Projects { get; }
|
||||
|
||||
ValueTask<ObservableCollection<CultivateEntryView>> GetCultivateEntriesAsync(CultivateProject cultivateProject, ICultivationMetadataContext context);
|
||||
@@ -17,13 +26,32 @@ internal interface ICultivationService
|
||||
ValueTask<ObservableCollection<StatisticsCultivateItem>> GetStatisticsCultivateItemCollectionAsync(
|
||||
CultivateProject cultivateProject, ICultivationMetadataContext context, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// 删除养成清单
|
||||
/// </summary>
|
||||
/// <param name="entryId">入口Id</param>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask RemoveCultivateEntryAsync(Guid entryId);
|
||||
|
||||
/// <summary>
|
||||
/// 异步移除项目
|
||||
/// </summary>
|
||||
/// <param name="project">项目</param>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask RemoveProjectAsync(CultivateProject project);
|
||||
|
||||
ValueTask<ConsumptionSaveResultKind> SaveConsumptionAsync(InputConsumption inputConsumption);
|
||||
ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<Item> items, LevelInformation levelInformation);
|
||||
|
||||
/// <summary>
|
||||
/// 保存养成物品状态
|
||||
/// </summary>
|
||||
/// <param name="item">养成物品</param>
|
||||
void SaveCultivateItem(CultivateItemView item);
|
||||
|
||||
/// <summary>
|
||||
/// 异步尝试添加新的项目
|
||||
/// </summary>
|
||||
/// <param name="project">项目</param>
|
||||
/// <returns>添加操作的结果</returns>
|
||||
ValueTask<ProjectAddResultKind> TryAddProjectAsync(CultivateProject project);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using CalculateItem = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Item;
|
||||
|
||||
namespace Snap.Hutao.Service.Cultivation;
|
||||
|
||||
internal sealed class InputConsumption
|
||||
{
|
||||
public required CultivateType Type { get; init; }
|
||||
|
||||
public required uint ItemId { get; init; }
|
||||
|
||||
public required List<CalculateItem> Items { get; init; }
|
||||
|
||||
public required LevelInformation LevelInformation { get; init; }
|
||||
|
||||
public required ConsumptionSaveStrategyKind Strategy { get; init; }
|
||||
}
|
||||
@@ -203,7 +203,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
|
||||
AsyncBarrier barrier = new(4);
|
||||
|
||||
List<HistoryWish> historyWishes = historyWishBuilders
|
||||
.Where(b => appOptions.IsEmptyHistoryWishVisible || !b.IsEmpty)
|
||||
.Where(b => appOptions.IsEmptyHistoryWishVisible || (!b.IsEmpty))
|
||||
.OrderByDescending(builder => builder.From)
|
||||
.ThenBy(builder => builder.ConfigType, GachaTypeComparer.Shared)
|
||||
.Select(builder => builder.ToHistoryWish())
|
||||
@@ -212,7 +212,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
|
||||
return new()
|
||||
{
|
||||
// history
|
||||
HistoryWishes = historyWishes.ToAdvancedCollectionView(),
|
||||
HistoryWishes = taskContext.InvokeOnMainThread(() => new AdvancedCollectionView<HistoryWish>(historyWishes)),
|
||||
|
||||
// avatars
|
||||
OrangeAvatars = orangeAvatarCounter.ToStatisticsList(),
|
||||
|
||||
@@ -47,6 +47,7 @@ internal sealed partial class GachaLogService : IGachaLogService
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
context = await metadataService.GetContextAsync<GachaLogServiceMetadataContext>(token).ConfigureAwait(false);
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
Archives = new(gachaLogDbService.GetGachaArchiveCollection(), serviceProvider);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -21,15 +21,9 @@ internal sealed partial class GameAccountService : IGameAccountService
|
||||
|
||||
private ObservableReorderableDbCollection<GameAccount>? gameAccounts;
|
||||
|
||||
public async ValueTask<ObservableReorderableDbCollection<GameAccount>> GetGameAccountCollectionAsync()
|
||||
public ObservableReorderableDbCollection<GameAccount> GameAccountCollection
|
||||
{
|
||||
if (gameAccounts is null)
|
||||
{
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
gameAccounts = gameDbService.GetGameAccountCollection();
|
||||
}
|
||||
|
||||
return gameAccounts;
|
||||
get => gameAccounts ??= gameDbService.GetGameAccountCollection();
|
||||
}
|
||||
|
||||
public async ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType)
|
||||
|
||||
@@ -9,14 +9,14 @@ namespace Snap.Hutao.Service.Game.Account;
|
||||
|
||||
internal interface IGameAccountService
|
||||
{
|
||||
ObservableReorderableDbCollection<GameAccount> GameAccountCollection { get; }
|
||||
|
||||
ValueTask AttachGameAccountToUidAsync(GameAccount gameAccount, string uid);
|
||||
|
||||
GameAccount? DetectCurrentGameAccount(SchemeType schemeType);
|
||||
|
||||
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType);
|
||||
|
||||
ValueTask<ObservableReorderableDbCollection<GameAccount>> GetGameAccountCollectionAsync();
|
||||
|
||||
ValueTask ModifyGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
@@ -11,6 +11,10 @@ using Snap.Hutao.Service.Game.PathAbstraction;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏服务
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton, typeof(IGameServiceFacade))]
|
||||
internal sealed partial class GameServiceFacade : IGameServiceFacade
|
||||
@@ -19,46 +23,55 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
|
||||
private readonly IGameAccountService gameAccountService;
|
||||
private readonly IGamePathService gamePathService;
|
||||
|
||||
public ValueTask<ObservableReorderableDbCollection<GameAccount>> GetGameAccountCollectionAsync()
|
||||
/// <inheritdoc/>
|
||||
public ObservableReorderableDbCollection<GameAccount> GameAccountCollection
|
||||
{
|
||||
return gameAccountService.GetGameAccountCollectionAsync();
|
||||
get => gameAccountService.GameAccountCollection;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<ValueResult<bool, string>> GetGamePathAsync()
|
||||
{
|
||||
return gamePathService.SilentGetGamePathAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ChannelOptions GetChannelOptions()
|
||||
{
|
||||
return gameChannelOptionsService.GetChannelOptions();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme)
|
||||
{
|
||||
return gameAccountService.DetectGameAccountAsync(scheme);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameAccount? DetectCurrentGameAccount(SchemeType scheme)
|
||||
{
|
||||
return gameAccountService.DetectCurrentGameAccount(scheme);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask AttachGameAccountToUidAsync(GameAccount gameAccount, string uid)
|
||||
{
|
||||
return gameAccountService.AttachGameAccountToUidAsync(gameAccount, uid);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
|
||||
{
|
||||
return gameAccountService.ModifyGameAccountAsync(gameAccount);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
|
||||
{
|
||||
return gameAccountService.RemoveGameAccountAsync(gameAccount);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsGameRunning()
|
||||
{
|
||||
return LaunchExecutionEnsureGameNotRunningHandler.IsGameRunning(out _);
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace Snap.Hutao.Service.Game;
|
||||
|
||||
internal interface IGameServiceFacade
|
||||
{
|
||||
ObservableReorderableDbCollection<GameAccount> GameAccountCollection { get; }
|
||||
|
||||
ValueTask AttachGameAccountToUidAsync(GameAccount gameAccount, string uid);
|
||||
|
||||
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme);
|
||||
@@ -25,6 +27,4 @@ internal interface IGameServiceFacade
|
||||
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
GameAccount? DetectCurrentGameAccount(SchemeType scheme);
|
||||
|
||||
ValueTask<ObservableReorderableDbCollection<GameAccount>> GetGameAccountCollectionAsync();
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using static Snap.Hutao.Win32.Kernel32;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
|
||||
/// <summary>
|
||||
@@ -15,34 +19,19 @@ internal static class GameFpsAddress
|
||||
|
||||
public static unsafe void UnsafeFindFpsAddress(GameFpsUnlockerContext context, in RequiredRemoteModule remoteModule, in RequiredLocalModule localModule)
|
||||
{
|
||||
Span<byte> executableSpan = localModule.Executable.AsSpan();
|
||||
int offsetToExecutable = 0;
|
||||
nuint localVirtualAddress = 0;
|
||||
do
|
||||
{
|
||||
int index = IndexOfPattern(executableSpan[offsetToExecutable..], out int patternLength);
|
||||
if (index < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
int offsetToUserAssembly = IndexOfPattern(localModule.UserAssembly.AsSpan());
|
||||
HutaoException.ThrowIfNot(offsetToUserAssembly >= 0, SH.ServiceGameUnlockerInterestedPatternNotFound);
|
||||
|
||||
offsetToExecutable += index;
|
||||
|
||||
nuint rip = localModule.Executable.Address + (uint)offsetToExecutable;
|
||||
nuint rip = localModule.UserAssembly.Address + (uint)offsetToUserAssembly;
|
||||
rip += 5U;
|
||||
rip += (nuint)(*(int*)(rip + 1U) + 5);
|
||||
rip += (nuint)(*(int*)(rip + 2U) + 6);
|
||||
|
||||
if (*(byte*)rip is ASM_JMP)
|
||||
{
|
||||
localVirtualAddress = rip;
|
||||
break;
|
||||
}
|
||||
nuint remoteVirtualAddress = remoteModule.UserAssembly.Address + (rip - localModule.UserAssembly.Address);
|
||||
|
||||
offsetToExecutable += patternLength;
|
||||
}
|
||||
while (true);
|
||||
nuint ptr = 0;
|
||||
SpinWait.SpinUntil(() => UnsafeReadProcessMemory(context.AllAccess, remoteVirtualAddress, out ptr) && ptr != 0);
|
||||
|
||||
ArgumentOutOfRangeException.ThrowIfZero(localVirtualAddress);
|
||||
nuint localVirtualAddress = ptr - remoteModule.UnityPlayer.Address + localModule.UnityPlayer.Address;
|
||||
|
||||
while (*(byte*)localVirtualAddress is ASM_CALL or ASM_JMP)
|
||||
{
|
||||
@@ -50,15 +39,22 @@ internal static class GameFpsAddress
|
||||
}
|
||||
|
||||
localVirtualAddress += *(uint*)(localVirtualAddress + 2) + 6;
|
||||
nuint relativeVirtualAddress = localVirtualAddress - localModule.Executable.Address;
|
||||
context.FpsAddress = remoteModule.Executable.Address + relativeVirtualAddress;
|
||||
nuint relativeVirtualAddress = localVirtualAddress - localModule.UnityPlayer.Address;
|
||||
context.FpsAddress = remoteModule.UnityPlayer.Address + relativeVirtualAddress;
|
||||
}
|
||||
|
||||
private static int IndexOfPattern(in ReadOnlySpan<byte> span, out int patternLength)
|
||||
private static int IndexOfPattern(in ReadOnlySpan<byte> memory)
|
||||
{
|
||||
// B9 3C 00 00 00 E8
|
||||
ReadOnlySpan<byte> part = [0xB9, 0x3C, 0x00, 0x00, 0x00, 0xE8];
|
||||
patternLength = part.Length;
|
||||
return span.IndexOf(part);
|
||||
// B9 3C 00 00 00 FF 15
|
||||
ReadOnlySpan<byte> part = [0xB9, 0x3C, 0x00, 0x00, 0x00, 0xFF, 0x15];
|
||||
return memory.IndexOf(part);
|
||||
}
|
||||
|
||||
private static unsafe bool UnsafeReadProcessMemory(HANDLE hProcess, nuint baseAddress, out nuint value)
|
||||
{
|
||||
value = 0;
|
||||
bool result = ReadProcessMemory(hProcess, (void*)baseAddress, ref value, out _);
|
||||
HutaoException.ThrowIfNot(result, SH.ServiceGameUnlockerReadProcessMemoryPointerAddressFailed);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -51,9 +51,12 @@ internal abstract class GameFpsUnlocker : IGameFpsUnlocker
|
||||
|
||||
private static RequiredLocalModule LoadRequiredLocalModule(GameFileSystem gameFileSystem)
|
||||
{
|
||||
string gameFoler = gameFileSystem.GameDirectory;
|
||||
string dataFoler = gameFileSystem.DataDirectory;
|
||||
LOAD_LIBRARY_FLAGS flags = LOAD_LIBRARY_FLAGS.LOAD_LIBRARY_AS_IMAGE_RESOURCE;
|
||||
HMODULE executaleAddress = LoadLibraryExW(gameFileSystem.GameFilePath, default, flags);
|
||||
HMODULE unityPlayerAddress = LoadLibraryExW(System.IO.Path.Combine(gameFoler, "UnityPlayer.dll"), default, flags);
|
||||
HMODULE userAssemblyAddress = LoadLibraryExW(System.IO.Path.Combine(dataFoler, "Native", "UserAssembly.dll"), default, flags);
|
||||
|
||||
return new(executaleAddress);
|
||||
return new(unityPlayerAddress, userAssemblyAddress);
|
||||
}
|
||||
}
|
||||
@@ -42,15 +42,16 @@ internal static class GameProcessModule
|
||||
|
||||
private static FindModuleResult UnsafeGetGameModuleInfo(in HANDLE hProcess, out RequiredRemoteModule info)
|
||||
{
|
||||
FindModuleResult result = UnsafeFindModule(hProcess, GameConstants.YuanShenFileName, GameConstants.GenshinImpactFileName, out Module executable);
|
||||
FindModuleResult unityPlayerResult = UnsafeFindModule(hProcess, "UnityPlayer.dll", out Module unityPlayer);
|
||||
FindModuleResult userAssemblyResult = UnsafeFindModule(hProcess, "UserAssembly.dll", out Module userAssembly);
|
||||
|
||||
if (result is FindModuleResult.Ok)
|
||||
if (unityPlayerResult is FindModuleResult.Ok && userAssemblyResult is FindModuleResult.Ok)
|
||||
{
|
||||
info = new(executable);
|
||||
info = new(unityPlayer, userAssembly);
|
||||
return FindModuleResult.Ok;
|
||||
}
|
||||
|
||||
if (result is FindModuleResult.NoModuleFound)
|
||||
if (unityPlayerResult is FindModuleResult.NoModuleFound && userAssemblyResult is FindModuleResult.NoModuleFound)
|
||||
{
|
||||
info = default;
|
||||
return FindModuleResult.NoModuleFound;
|
||||
@@ -60,7 +61,7 @@ internal static class GameProcessModule
|
||||
return FindModuleResult.ModuleNotLoaded;
|
||||
}
|
||||
|
||||
private static unsafe FindModuleResult UnsafeFindModule(in HANDLE hProcess, in ReadOnlySpan<char> moduleName1, in ReadOnlySpan<char> moduleName2, out Module module)
|
||||
private static unsafe FindModuleResult UnsafeFindModule(in HANDLE hProcess, in ReadOnlySpan<char> moduleName, out Module module)
|
||||
{
|
||||
HMODULE[] buffer = new HMODULE[128];
|
||||
if (!K32EnumProcessModules(hProcess, buffer, out uint actualSize))
|
||||
@@ -85,8 +86,7 @@ internal static class GameProcessModule
|
||||
|
||||
fixed (char* lpBaseName = baseName)
|
||||
{
|
||||
ReadOnlySpan<char> baseNameSpan = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(lpBaseName);
|
||||
if (!(moduleName1.SequenceEqual(baseNameSpan) || moduleName2.SequenceEqual(baseNameSpan)))
|
||||
if (!moduleName.SequenceEqual(MemoryMarshal.CreateReadOnlySpanFromNullTerminated(lpBaseName)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -12,24 +12,30 @@ namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
internal readonly struct RequiredLocalModule : IDisposable
|
||||
{
|
||||
public readonly bool HasValue = false;
|
||||
public readonly Module Executable;
|
||||
public readonly Module UnityPlayer;
|
||||
public readonly Module UserAssembly;
|
||||
|
||||
private readonly HMODULE hModuleExecutable;
|
||||
private readonly HMODULE hModuleUnityPlayer;
|
||||
private readonly HMODULE hModuleUserAssembly;
|
||||
|
||||
public RequiredLocalModule(HMODULE executable)
|
||||
public RequiredLocalModule(HMODULE unityPlayer, HMODULE userAssembly)
|
||||
{
|
||||
hModuleExecutable = executable;
|
||||
hModuleUnityPlayer = unityPlayer;
|
||||
hModuleUserAssembly = userAssembly;
|
||||
|
||||
// Align the pointer
|
||||
nint executableMappedView = (nint)(executable & ~0x3L);
|
||||
nint unityPlayerMappedView = (nint)(unityPlayer & ~0x3L);
|
||||
nint userAssemblyMappedView = (nint)(userAssembly & ~0x3L);
|
||||
|
||||
Executable = new((nuint)executableMappedView, GetImageSize(executableMappedView));
|
||||
HasValue = true;
|
||||
UnityPlayer = new((nuint)unityPlayerMappedView, GetImageSize(unityPlayerMappedView));
|
||||
UserAssembly = new((nuint)userAssemblyMappedView, GetImageSize(userAssemblyMappedView));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
FreeLibrary(hModuleExecutable);
|
||||
FreeLibrary(hModuleUnityPlayer);
|
||||
FreeLibrary(hModuleUserAssembly);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
||||
@@ -6,11 +6,13 @@ namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
internal readonly struct RequiredRemoteModule
|
||||
{
|
||||
public readonly bool HasValue = false;
|
||||
public readonly Module Executable;
|
||||
public readonly Module UnityPlayer;
|
||||
public readonly Module UserAssembly;
|
||||
|
||||
public RequiredRemoteModule(in Module executable)
|
||||
public RequiredRemoteModule(in Module unityPlayer, in Module userAssembly)
|
||||
{
|
||||
HasValue = true;
|
||||
Executable = executable;
|
||||
UnityPlayer = unityPlayer;
|
||||
UserAssembly = userAssembly;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
using Snap.Hutao.Model.Entity.Extension;
|
||||
using Snap.Hutao.UI.Xaml.Data;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
using Snap.Hutao.Web.Hoyolab.Passport;
|
||||
@@ -30,7 +29,7 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
|
||||
user.UserInfo = new() { Nickname = SH.ModelBindingUserInitializationFailed };
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
user.UserGameRoles = new([]);
|
||||
user.UserGameRoles = [];
|
||||
}
|
||||
|
||||
return user;
|
||||
@@ -214,7 +213,8 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
|
||||
|
||||
if (userGameRolesResponse.IsOk())
|
||||
{
|
||||
user.UserGameRoles = userGameRolesResponse.Data.List.ToAdvancedCollectionView();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
user.UserGameRoles = new(userGameRolesResponse.Data.List);
|
||||
return user.UserGameRoles.Count > 0;
|
||||
}
|
||||
else
|
||||
|
||||
@@ -99,12 +99,8 @@ internal sealed class UniformStaggeredLayoutState
|
||||
}
|
||||
|
||||
internal void Clear()
|
||||
{
|
||||
if (items.Count > 0)
|
||||
{
|
||||
RecycleElements();
|
||||
}
|
||||
|
||||
ClearColumns();
|
||||
ClearItems();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using CommunityToolkit.WinUI.Collections;
|
||||
using CommunityToolkit.WinUI.Helpers;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using System.Collections;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
@@ -27,6 +28,11 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
|
||||
private int deferCounter;
|
||||
private WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>? sourceWeakEventListener;
|
||||
|
||||
public AdvancedCollectionView()
|
||||
: this([])
|
||||
{
|
||||
}
|
||||
|
||||
public AdvancedCollectionView(IList<T> source)
|
||||
{
|
||||
view = [];
|
||||
@@ -622,13 +628,14 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
|
||||
|
||||
if (i < -1 || i >= view.Count)
|
||||
{
|
||||
// view is empty, i is 0, current pos is -1
|
||||
Debugger.Break(); // Figure out how this will hit.
|
||||
OnPropertyChanged(nameof(CurrentItem));
|
||||
return false;
|
||||
}
|
||||
|
||||
OnCurrentChanging(out bool cancel);
|
||||
if (cancel)
|
||||
CurrentChangingEventArgs e = new();
|
||||
OnCurrentChanging(e);
|
||||
if (e.Cancel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -638,17 +645,14 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnCurrentChanging(out bool cancel)
|
||||
private void OnCurrentChanging(CurrentChangingEventArgs e)
|
||||
{
|
||||
if (!created || deferCounter > 0)
|
||||
{
|
||||
cancel = false;
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentChangingEventArgs e = new();
|
||||
CurrentChanging?.Invoke(this, e);
|
||||
cancel = e.Cancel;
|
||||
}
|
||||
|
||||
private void OnCurrentChanged()
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.UI.Xaml.Data;
|
||||
|
||||
internal static class AdvancedCollectionViewExtension
|
||||
@@ -15,11 +13,4 @@ internal static class AdvancedCollectionViewExtension
|
||||
view.MoveCurrentTo(default);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static AdvancedCollectionView<T> ToAdvancedCollectionView<T>(this IList<T> source)
|
||||
where T : class, IAdvancedCollectionViewItem
|
||||
{
|
||||
return new AdvancedCollectionView<T>(source);
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@
|
||||
</cwc:HeaderedContentControl>
|
||||
</cwc:UniformGrid>
|
||||
<RadioButtons
|
||||
x:Name="ImportModeSelector"
|
||||
Name="ImportModeSelector"
|
||||
Grid.Row="1"
|
||||
Margin="0,16,0,0"
|
||||
Header="{shuxm:ResourceString Name=ViewDialogAchievementArchiveImportStrategy}"
|
||||
|
||||
@@ -8,11 +8,20 @@ using Snap.Hutao.Service.Achievement;
|
||||
|
||||
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
||||
|
||||
/// <summary>
|
||||
/// 成就对话框
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("UIAF", typeof(UIAF))]
|
||||
internal sealed partial class AchievementImportDialog : ContentDialog
|
||||
{
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的成就对话框
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="uiaf">uiaf数据</param>
|
||||
public AchievementImportDialog(IServiceProvider serviceProvider, UIAF uiaf)
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -21,6 +30,10 @@ internal sealed partial class AchievementImportDialog : ContentDialog
|
||||
UIAF = uiaf;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取导入选项
|
||||
/// </summary>
|
||||
/// <returns>导入选项</returns>
|
||||
public async ValueTask<ValueResult<bool, ImportStrategyKind>> GetImportStrategyAsync()
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
@@ -73,15 +73,5 @@
|
||||
SpinButtonPlacementMode="Inline"
|
||||
Value="{x:Bind PromotionDelta.Weapon.LevelTarget, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
|
||||
<RadioButtons
|
||||
x:Name="SaveModeSelector"
|
||||
Margin="0,13,0,0"
|
||||
Header="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyHeader}"
|
||||
SelectedIndex="0">
|
||||
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyPreserveExisting}"/>
|
||||
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting}"/>
|
||||
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry}"/>
|
||||
</RadioButtons>
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Service.Cultivation;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
|
||||
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
||||
@@ -22,7 +21,7 @@ internal sealed partial class CultivatePromotionDeltaBatchDialog : ContentDialog
|
||||
PromotionDelta = AvatarPromotionDelta.CreateForBaseline();
|
||||
}
|
||||
|
||||
public async ValueTask<ValueResult<bool, CultivatePromotionDeltaOptions>> GetPromotionDeltaBaselineAsync()
|
||||
public async ValueTask<ValueResult<bool, AvatarPromotionDelta>> GetPromotionDeltaBaselineAsync()
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
ContentDialogResult result = await ShowAsync();
|
||||
@@ -51,6 +50,6 @@ internal sealed partial class CultivatePromotionDeltaBatchDialog : ContentDialog
|
||||
LocalSetting.Set(SettingKeys.CultivationWeapon90LevelTarget, weapon.LevelTarget);
|
||||
}
|
||||
|
||||
return new(true, new(PromotionDelta, (ConsumptionSaveStrategyKind)SaveModeSelector.SelectedIndex));
|
||||
return new(true, PromotionDelta);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<DataTemplate x:Key="SkillTemplate">
|
||||
<Border Margin="0,2,0,0" Style="{StaticResource BorderCardStyle}">
|
||||
<Grid Margin="8" ColumnSpacing="6">
|
||||
<Grid Margin="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition Width="160"/>
|
||||
@@ -64,17 +64,18 @@
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ContentDialog.Resources>
|
||||
<Grid Margin="0,8,0,0" RowSpacing="6">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0" Visibility="{x:Bind Avatar, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Margin="0,8,0,0"
|
||||
Visibility="{x:Bind Avatar, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
|
||||
<Grid
|
||||
Padding="8"
|
||||
ColumnSpacing="6"
|
||||
DataContext="{x:Bind Avatar}"
|
||||
Style="{ThemeResource GridCardStyle}">
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -122,10 +123,12 @@
|
||||
<ItemsControl ItemTemplate="{StaticResource SkillTemplate}" ItemsSource="{x:Bind Avatar.Skills}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="1" Visibility="{x:Bind Weapon, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="0,6,0,0"
|
||||
Visibility="{x:Bind Weapon, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
|
||||
<Grid
|
||||
Padding="8"
|
||||
ColumnSpacing="6"
|
||||
DataContext="{x:Bind Weapon}"
|
||||
Style="{ThemeResource GridCardStyle}">
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -171,15 +174,5 @@
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<RadioButtons
|
||||
x:Name="SaveModeSelector"
|
||||
Grid.Row="2"
|
||||
Margin="0,10,0,0"
|
||||
Header="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyHeader}"
|
||||
SelectedIndex="0">
|
||||
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyPreserveExisting}"/>
|
||||
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting}"/>
|
||||
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry}"/>
|
||||
</RadioButtons>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
@@ -4,17 +4,25 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Model.Calculable;
|
||||
using Snap.Hutao.Service.Cultivation;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
|
||||
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
||||
|
||||
/// <summary>
|
||||
/// 养成计算对话框
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("Avatar", typeof(ICalculableAvatar))]
|
||||
[DependencyProperty("Weapon", typeof(ICalculableWeapon))]
|
||||
internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
|
||||
{
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的养成计算对话框
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="options">选项</param>
|
||||
public CultivatePromotionDeltaDialog(IServiceProvider serviceProvider, CalculableOptions options)
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -25,7 +33,11 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
|
||||
Weapon = options.Weapon;
|
||||
}
|
||||
|
||||
public async ValueTask<ValueResult<bool, CultivatePromotionDeltaOptions>> GetPromotionDeltaAsync()
|
||||
/// <summary>
|
||||
/// 异步获取提升差异
|
||||
/// </summary>
|
||||
/// <returns>提升差异</returns>
|
||||
public async ValueTask<ValueResult<bool, AvatarPromotionDelta>> GetPromotionDeltaAsync()
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
ContentDialogResult result = await ShowAsync();
|
||||
@@ -54,6 +66,6 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
|
||||
},
|
||||
};
|
||||
|
||||
return new(true, new(delta, (ConsumptionSaveStrategyKind)SaveModeSelector.SelectedIndex));
|
||||
return new(true, delta);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Service.Cultivation;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
|
||||
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
||||
|
||||
internal sealed class CultivatePromotionDeltaOptions
|
||||
{
|
||||
public CultivatePromotionDeltaOptions(AvatarPromotionDelta delta, ConsumptionSaveStrategyKind strategy)
|
||||
{
|
||||
Delta = delta;
|
||||
Strategy = strategy;
|
||||
}
|
||||
|
||||
public AvatarPromotionDelta Delta { get; set; }
|
||||
|
||||
public ConsumptionSaveStrategyKind Strategy { get; set; }
|
||||
}
|
||||
@@ -107,16 +107,14 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> ReceiveAsync(INavigationData data)
|
||||
{
|
||||
if (!await Initialization.Task.ConfigureAwait(false))
|
||||
if (await Initialization.Task.ConfigureAwait(false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data.Data is AppActivation.ImportUIAFFromClipboard)
|
||||
{
|
||||
await ImportUIAFFromClipboardAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -128,7 +126,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
|
||||
return false;
|
||||
}
|
||||
|
||||
AdvancedCollectionView<AchievementGoalView> sortedGoals;
|
||||
List<AchievementGoalView> sortedGoals;
|
||||
|
||||
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
|
||||
{
|
||||
@@ -136,13 +134,13 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
|
||||
.GetAchievementGoalListAsync(CancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
sortedGoals = goals.SortBy(goal => goal.Order).SelectList(AchievementGoalView.From).ToAdvancedCollectionView();
|
||||
sortedGoals = goals.SortBy(goal => goal.Order).SelectList(AchievementGoalView.From);
|
||||
}
|
||||
|
||||
IAdvancedDbCollectionView<EntityArchive> archives = await scopeContext.AchievementService.GetArchivesAsync(CancellationToken).ConfigureAwait(false);
|
||||
await scopeContext.TaskContext.SwitchToMainThreadAsync();
|
||||
|
||||
AchievementGoals = sortedGoals;
|
||||
AchievementGoals = new(sortedGoals);
|
||||
Archives = archives;
|
||||
Archives.MoveCurrentTo(Archives.SourceCollection.SelectedOrDefault());
|
||||
return true;
|
||||
@@ -208,7 +206,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
|
||||
[Command("RemoveArchiveCommand")]
|
||||
private async Task RemoveArchiveAsync()
|
||||
{
|
||||
if (Archives?.CurrentItem is not { } current)
|
||||
if (Archives is null || !(Archives.CurrentItem is { } current))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -285,9 +283,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
|
||||
|
||||
private async ValueTask UpdateAchievementsAsync(EntityArchive? archive)
|
||||
{
|
||||
await scopeContext.TaskContext.SwitchToMainThreadAsync();
|
||||
Achievements = default;
|
||||
|
||||
// TODO: immediately clear values
|
||||
if (archive is null)
|
||||
{
|
||||
return;
|
||||
@@ -297,29 +293,29 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
|
||||
.GetContextAsync<AchievementServiceMetadataContext>(CancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!TryGetAchievements(archive, context, out AdvancedCollectionView<AchievementView>? combined))
|
||||
if (!TryGetAchievements(archive, context, out List<AchievementView>? combined))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await scopeContext.TaskContext.SwitchToMainThreadAsync();
|
||||
Achievements = combined;
|
||||
Achievements = new(combined);
|
||||
AchievementFinishPercent.Update(this);
|
||||
UpdateAchievementsFilterByGoal(AchievementGoals?.CurrentItem);
|
||||
UpdateAchievementsSort();
|
||||
}
|
||||
|
||||
private bool TryGetAchievements(EntityArchive archive, AchievementServiceMetadataContext context, [NotNullWhen(true)] out AdvancedCollectionView<AchievementView>? view)
|
||||
private bool TryGetAchievements(EntityArchive archive, AchievementServiceMetadataContext context, [NotNullWhen(true)] out List<AchievementView>? combined)
|
||||
{
|
||||
try
|
||||
{
|
||||
view = scopeContext.AchievementService.GetAchievementViewList(archive, context).ToAdvancedCollectionView();
|
||||
combined = scopeContext.AchievementService.GetAchievementViewList(archive, context);
|
||||
return true;
|
||||
}
|
||||
catch (HutaoException ex)
|
||||
{
|
||||
scopeContext.InfoBarService.Error(ex);
|
||||
view = default;
|
||||
combined = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -332,10 +328,6 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
|
||||
return;
|
||||
}
|
||||
|
||||
using (Achievements.DeferRefresh())
|
||||
{
|
||||
using (AchievementGoals.DeferRefresh())
|
||||
{
|
||||
Achievements.SortDescriptions.Clear();
|
||||
AchievementGoals.SortDescriptions.Clear();
|
||||
|
||||
@@ -349,16 +341,11 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
|
||||
Achievements.SortDescriptions.Add(achievementDefaultSortDescription);
|
||||
AchievementGoals.SortDescriptions.Add(achievementGoalDefaultSortDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAchievementsFilterByGoal(AchievementGoalView? goal)
|
||||
{
|
||||
if (Achievements is null)
|
||||
if (Achievements is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (goal is null)
|
||||
{
|
||||
Achievements.Filter = default!;
|
||||
@@ -369,6 +356,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
|
||||
Achievements.Filter = (AchievementView view) => view.Inner.Goal == goalId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Command("SearchAchievementCommand")]
|
||||
private void UpdateAchievementsFilterBySearch(string? search)
|
||||
@@ -408,12 +396,10 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
|
||||
[Command("SaveAchievementCommand")]
|
||||
private void SaveAchievement(AchievementView? achievement)
|
||||
{
|
||||
if (achievement is null)
|
||||
if (achievement is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
scopeContext.AchievementService.SaveAchievement(achievement);
|
||||
AchievementFinishPercent.Update(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Core.DataTransfer;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Model.Calculable;
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using Snap.Hutao.Service.AvatarInfo;
|
||||
@@ -18,7 +20,9 @@ using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using CalculatorAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
|
||||
using CalculatorBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption;
|
||||
using CalculatorClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
|
||||
using CalculatorConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
|
||||
using CalculatorItem = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Item;
|
||||
using CalculatorItemHelper = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.ItemHelper;
|
||||
|
||||
namespace Snap.Hutao.ViewModel.AvatarProperty;
|
||||
@@ -27,7 +31,14 @@ namespace Snap.Hutao.ViewModel.AvatarProperty;
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, IRecipient<UserAndUidChangedMessage>
|
||||
{
|
||||
private readonly AvatarPropertyViewModelScopeContext scopeContext;
|
||||
private readonly IContentDialogFactory contentDialogFactory;
|
||||
private readonly ICultivationService cultivationService;
|
||||
private readonly IAvatarInfoService avatarInfoService;
|
||||
private readonly IClipboardProvider clipboardProvider;
|
||||
private readonly CalculatorClient calculatorClient;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly ITaskContext taskContext;
|
||||
private readonly IUserService userService;
|
||||
|
||||
private Summary? summary;
|
||||
|
||||
@@ -50,7 +61,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
||||
|
||||
protected override async ValueTask<bool> InitializeOverrideAsync()
|
||||
{
|
||||
if (await scopeContext.UserService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
|
||||
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
|
||||
{
|
||||
await RefreshCoreAsync(userAndUid, RefreshOption.None, CancellationToken).ConfigureAwait(false);
|
||||
return true;
|
||||
@@ -62,7 +73,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
||||
[Command("RefreshFromEnkaApiCommand")]
|
||||
private async Task RefreshByEnkaApiAsync()
|
||||
{
|
||||
if (await scopeContext.UserService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
|
||||
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
|
||||
{
|
||||
await RefreshCoreAsync(userAndUid, RefreshOption.RequestFromEnkaAPI, CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -71,7 +82,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
||||
[Command("RefreshFromHoyolabGameRecordCommand")]
|
||||
private async Task RefreshByHoyolabGameRecordAsync()
|
||||
{
|
||||
if (await scopeContext.UserService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
|
||||
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
|
||||
{
|
||||
await RefreshCoreAsync(userAndUid, RefreshOption.RequestFromHoyolabGameRecord, CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -80,7 +91,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
||||
[Command("RefreshFromHoyolabCalculateCommand")]
|
||||
private async Task RefreshByHoyolabCalculateAsync()
|
||||
{
|
||||
if (await scopeContext.UserService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
|
||||
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
|
||||
{
|
||||
await RefreshCoreAsync(userAndUid, RefreshOption.RequestFromHoyolabCalculate, CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -90,19 +101,18 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
||||
{
|
||||
try
|
||||
{
|
||||
await scopeContext.TaskContext.SwitchToMainThreadAsync();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
IsInitialized = false;
|
||||
|
||||
ValueResult<RefreshResultKind, Summary?> summaryResult;
|
||||
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
|
||||
{
|
||||
ContentDialog dialog = await scopeContext.ContentDialogFactory
|
||||
ContentDialog dialog = await contentDialogFactory
|
||||
.CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyFetch)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
using (await dialog.BlockAsync(scopeContext.TaskContext).ConfigureAwait(false))
|
||||
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
|
||||
{
|
||||
summaryResult = await scopeContext.AvatarInfoService
|
||||
summaryResult = await avatarInfoService
|
||||
.GetSummaryAsync(userAndUid, option, token)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
@@ -111,7 +121,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
||||
(RefreshResultKind result, Summary? summary) = summaryResult;
|
||||
if (result is RefreshResultKind.Ok)
|
||||
{
|
||||
await scopeContext.TaskContext.SwitchToMainThreadAsync();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
Summary = summary;
|
||||
Summary?.Avatars.MoveCurrentToFirstOrDefault();
|
||||
}
|
||||
@@ -120,16 +130,16 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
||||
switch (result)
|
||||
{
|
||||
case RefreshResultKind.APIUnavailable:
|
||||
scopeContext.InfoBarService.Warning(SH.ViewModelAvatarPropertyEnkaApiUnavailable);
|
||||
infoBarService.Warning(SH.ViewModelAvatarPropertyEnkaApiUnavailable);
|
||||
break;
|
||||
|
||||
case RefreshResultKind.StatusCodeNotSucceed:
|
||||
ArgumentNullException.ThrowIfNull(summary);
|
||||
scopeContext.InfoBarService.Warning(summary.Message);
|
||||
infoBarService.Warning(summary.Message);
|
||||
break;
|
||||
|
||||
case RefreshResultKind.ShowcaseNotOpen:
|
||||
scopeContext.InfoBarService.Warning(SH.ViewModelAvatarPropertyShowcaseNotOpen);
|
||||
infoBarService.Warning(SH.ViewModelAvatarPropertyShowcaseNotOpen);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -139,7 +149,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
||||
}
|
||||
finally
|
||||
{
|
||||
await scopeContext.TaskContext.SwitchToMainThreadAsync();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
IsInitialized = true;
|
||||
}
|
||||
}
|
||||
@@ -152,47 +162,41 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
||||
return;
|
||||
}
|
||||
|
||||
if (await scopeContext.UserService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
|
||||
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
|
||||
{
|
||||
scopeContext.InfoBarService.Warning(SH.MustSelectUserAndUid);
|
||||
infoBarService.Warning(SH.MustSelectUserAndUid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (avatar.Weapon is null)
|
||||
{
|
||||
scopeContext.InfoBarService.Warning(SH.ViewModelAvatarPropertyCalculateWeaponNull);
|
||||
infoBarService.Warning(SH.ViewModelAvatarPropertyCalculateWeaponNull);
|
||||
return;
|
||||
}
|
||||
|
||||
CalculableOptions options = new(avatar.ToCalculable(), avatar.Weapon.ToCalculable());
|
||||
CultivatePromotionDeltaDialog dialog = await scopeContext.ContentDialogFactory
|
||||
.CreateInstanceAsync<CultivatePromotionDeltaDialog>(options).ConfigureAwait(false);
|
||||
(bool isOk, CultivatePromotionDeltaOptions deltaOptions) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
|
||||
CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync<CultivatePromotionDeltaDialog>(options).ConfigureAwait(false);
|
||||
(bool isOk, CalculatorAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
|
||||
|
||||
if (!isOk)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Response<CalculatorBatchConsumption> response;
|
||||
using (IServiceScope scope = scopeContext.ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
CalculateClient calculatorClient = scope.ServiceProvider.GetRequiredService<CalculateClient>();
|
||||
response = await calculatorClient.BatchComputeAsync(userAndUid, deltaOptions.Delta).ConfigureAwait(false);
|
||||
}
|
||||
Response<CalculatorBatchConsumption> response = await calculatorClient.BatchComputeAsync(userAndUid, delta).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsOk())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await SaveCultivationAsync(response.Data.Items.Single(), deltaOptions).ConfigureAwait(false))
|
||||
if (!await SaveCultivationAsync(response.Data.Items.Single(), delta).ConfigureAwait(false))
|
||||
{
|
||||
scopeContext.InfoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
|
||||
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
|
||||
return;
|
||||
}
|
||||
|
||||
scopeContext.InfoBarService.Success(SH.ViewModelCultivationEntryAddSuccess);
|
||||
infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess);
|
||||
}
|
||||
|
||||
[Command("BatchCultivateCommand")]
|
||||
@@ -203,35 +207,34 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
||||
return;
|
||||
}
|
||||
|
||||
if (await scopeContext.UserService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
|
||||
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
|
||||
{
|
||||
scopeContext.InfoBarService.Warning(SH.MustSelectUserAndUid);
|
||||
infoBarService.Warning(SH.MustSelectUserAndUid);
|
||||
return;
|
||||
}
|
||||
|
||||
CultivatePromotionDeltaBatchDialog dialog = await scopeContext.ContentDialogFactory
|
||||
.CreateInstanceAsync<CultivatePromotionDeltaBatchDialog>().ConfigureAwait(false);
|
||||
(bool isOk, CultivatePromotionDeltaOptions deltaOptions) = await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false);
|
||||
CultivatePromotionDeltaBatchDialog dialog = await contentDialogFactory.CreateInstanceAsync<CultivatePromotionDeltaBatchDialog>().ConfigureAwait(false);
|
||||
(bool isOk, CalculatorAvatarPromotionDelta baseline) = await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false);
|
||||
|
||||
if (!isOk)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(deltaOptions.Delta.SkillList);
|
||||
ArgumentNullException.ThrowIfNull(deltaOptions.Delta.Weapon);
|
||||
ArgumentNullException.ThrowIfNull(baseline.SkillList);
|
||||
ArgumentNullException.ThrowIfNull(baseline.Weapon);
|
||||
|
||||
ContentDialog progressDialog = await scopeContext.ContentDialogFactory
|
||||
ContentDialog progressDialog = await contentDialogFactory
|
||||
.CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyBatchCultivateProgressTitle)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
BatchCultivateResult result = default;
|
||||
using (await progressDialog.BlockAsync(scopeContext.TaskContext).ConfigureAwait(false))
|
||||
using (await progressDialog.BlockAsync(taskContext).ConfigureAwait(false))
|
||||
{
|
||||
List<CalculatorAvatarPromotionDelta> deltas = [];
|
||||
foreach (AvatarView avatar in avatars)
|
||||
{
|
||||
if (!deltaOptions.Delta.TryGetNonErrorCopy(avatar, out CalculatorAvatarPromotionDelta? copy))
|
||||
if (!baseline.TryGetNonErrorCopy(avatar, out CalculatorAvatarPromotionDelta? copy))
|
||||
{
|
||||
++result.SkippedCount;
|
||||
continue;
|
||||
@@ -240,12 +243,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
||||
deltas.Add(copy);
|
||||
}
|
||||
|
||||
Response<CalculatorBatchConsumption> response;
|
||||
using (IServiceScope scope = scopeContext.ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
CalculateClient calculatorClient = scope.ServiceProvider.GetRequiredService<CalculateClient>();
|
||||
response = await calculatorClient.BatchComputeAsync(userAndUid, deltas).ConfigureAwait(false);
|
||||
}
|
||||
Response<CalculatorBatchConsumption> response = await calculatorClient.BatchComputeAsync(userAndUid, deltas).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsOk())
|
||||
{
|
||||
@@ -254,8 +252,9 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
||||
|
||||
foreach ((CalculatorConsumption consumption, CalculatorAvatarPromotionDelta delta) in response.Data.Items.Zip(deltas))
|
||||
{
|
||||
if (!await SaveCultivationAsync(consumption, new(delta, deltaOptions.Strategy)).ConfigureAwait(false))
|
||||
if (!await SaveCultivationAsync(consumption, delta).ConfigureAwait(false))
|
||||
{
|
||||
result.Interrupted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -263,70 +262,42 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
||||
}
|
||||
}
|
||||
|
||||
if (result.SkippedCount > 0)
|
||||
if (result.Interrupted)
|
||||
{
|
||||
scopeContext.InfoBarService.Warning(SH.FormatViewModelCultivationBatchAddIncompletedFormat(result.SucceedCount, result.SkippedCount));
|
||||
infoBarService.Warning(SH.FormatViewModelCultivationBatchAddIncompletedFormat(result.SucceedCount, result.SkippedCount));
|
||||
}
|
||||
else
|
||||
{
|
||||
scopeContext.InfoBarService.Success(SH.FormatViewModelCultivationBatchAddCompletedFormat(result.SucceedCount, result.SkippedCount));
|
||||
infoBarService.Success(SH.FormatViewModelCultivationBatchAddCompletedFormat(result.SucceedCount, result.SkippedCount));
|
||||
}
|
||||
}
|
||||
|
||||
/// <returns><see langword="true"/> if we can continue saving consumptions, otherwise <see langword="false"/>.</returns>
|
||||
private async ValueTask<bool> SaveCultivationAsync(CalculatorConsumption consumption, CultivatePromotionDeltaOptions options)
|
||||
private async ValueTask<bool> SaveCultivationAsync(CalculatorConsumption consumption, CalculatorAvatarPromotionDelta delta)
|
||||
{
|
||||
LevelInformation levelInformation = LevelInformation.From(options.Delta);
|
||||
LevelInformation levelInformation = LevelInformation.From(delta);
|
||||
|
||||
InputConsumption avatarInput = new()
|
||||
{
|
||||
Type = CultivateType.AvatarAndSkill,
|
||||
ItemId = options.Delta.AvatarId,
|
||||
Items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume),
|
||||
LevelInformation = levelInformation,
|
||||
Strategy = options.Strategy,
|
||||
};
|
||||
|
||||
ConsumptionSaveResultKind avatarSaveKind = await scopeContext.CultivationService.SaveConsumptionAsync(avatarInput).ConfigureAwait(false);
|
||||
|
||||
switch (avatarSaveKind)
|
||||
{
|
||||
case ConsumptionSaveResultKind.NoProject:
|
||||
scopeContext.InfoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
|
||||
return false;
|
||||
case ConsumptionSaveResultKind.Skipped:
|
||||
scopeContext.InfoBarService.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint);
|
||||
break;
|
||||
case ConsumptionSaveResultKind.NoItem:
|
||||
scopeContext.InfoBarService.Information(SH.ViewModelCultivationConsumptionSaveNoItemHint);
|
||||
break;
|
||||
case ConsumptionSaveResultKind.Added:
|
||||
break;
|
||||
}
|
||||
List<CalculatorItem> items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume);
|
||||
bool avatarSaved = await cultivationService
|
||||
.SaveConsumptionAsync(CultivateType.AvatarAndSkill, delta.AvatarId, items, levelInformation)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options.Delta.Weapon);
|
||||
ArgumentNullException.ThrowIfNull(delta.Weapon);
|
||||
|
||||
InputConsumption weaponInput = new()
|
||||
{
|
||||
Type = CultivateType.Weapon,
|
||||
ItemId = options.Delta.Weapon.Id,
|
||||
Items = consumption.WeaponConsume.EmptyIfNull(),
|
||||
LevelInformation = levelInformation,
|
||||
Strategy = options.Strategy,
|
||||
};
|
||||
// Take a hot path if avatar is not saved.
|
||||
bool avatarAndWeaponSaved = avatarSaved && await cultivationService
|
||||
.SaveConsumptionAsync(CultivateType.Weapon, delta.Weapon.Id, consumption.WeaponConsume.EmptyIfNull(), levelInformation)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
ConsumptionSaveResultKind weaponSaveKind = await scopeContext.CultivationService.SaveConsumptionAsync(weaponInput).ConfigureAwait(false);
|
||||
|
||||
return weaponSaveKind is not ConsumptionSaveResultKind.NoProject;
|
||||
return avatarAndWeaponSaved;
|
||||
}
|
||||
catch (HutaoException ex)
|
||||
{
|
||||
scopeContext.InfoBarService.Error(ex, SH.ViewModelCultivationAddWarning);
|
||||
infoBarService.Error(ex, SH.ViewModelCultivationAddWarning);
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
[Command("ExportToTextCommand")]
|
||||
@@ -337,13 +308,13 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
|
||||
return;
|
||||
}
|
||||
|
||||
if (scopeContext.ClipboardProvider.SetText(AvatarViewTextTemplating.GetTemplatedText(avatar)))
|
||||
if (clipboardProvider.SetText(AvatarViewTextTemplating.GetTemplatedText(avatar)))
|
||||
{
|
||||
scopeContext.InfoBarService.Success(SH.ViewModelAvatatPropertyExportTextSuccess);
|
||||
infoBarService.Success(SH.ViewModelAvatatPropertyExportTextSuccess);
|
||||
}
|
||||
else
|
||||
{
|
||||
scopeContext.InfoBarService.Warning(SH.ViewModelAvatatPropertyExportTextError);
|
||||
infoBarService.Warning(SH.ViewModelAvatatPropertyExportTextError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DataTransfer;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Service.AvatarInfo;
|
||||
using Snap.Hutao.Service.Cultivation;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using Snap.Hutao.Service.User;
|
||||
|
||||
namespace Snap.Hutao.ViewModel.AvatarProperty;
|
||||
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal sealed partial class AvatarPropertyViewModelScopeContext
|
||||
{
|
||||
private readonly IContentDialogFactory contentDialogFactory;
|
||||
private readonly IServiceScopeFactory serviceScopeFactory;
|
||||
private readonly ICultivationService cultivationService;
|
||||
private readonly IAvatarInfoService avatarInfoService;
|
||||
private readonly IClipboardProvider clipboardProvider;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly ITaskContext taskContext;
|
||||
private readonly IUserService userService;
|
||||
|
||||
public IContentDialogFactory ContentDialogFactory { get => contentDialogFactory; }
|
||||
|
||||
public IServiceScopeFactory ServiceScopeFactory { get => serviceScopeFactory; }
|
||||
|
||||
public ICultivationService CultivationService { get => cultivationService; }
|
||||
|
||||
public IAvatarInfoService AvatarInfoService { get => avatarInfoService; }
|
||||
|
||||
public IClipboardProvider ClipboardProvider { get => clipboardProvider; }
|
||||
|
||||
public IInfoBarService InfoBarService { get => infoBarService; }
|
||||
|
||||
public ITaskContext TaskContext { get => taskContext; }
|
||||
|
||||
public IUserService UserService { get => userService; }
|
||||
}
|
||||
@@ -7,4 +7,5 @@ internal struct BatchCultivateResult
|
||||
{
|
||||
public int SucceedCount;
|
||||
public int SkippedCount;
|
||||
public bool Interrupted;
|
||||
}
|
||||
@@ -325,11 +325,13 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
|
||||
async ValueTask UpdateGameAccountsViewAsync()
|
||||
{
|
||||
gameAccountFilter = new(SelectedScheme?.GetSchemeType());
|
||||
ObservableReorderableDbCollection<GameAccount> accounts = await gameService.GetGameAccountCollectionAsync().ConfigureAwait(false);
|
||||
AdvancedCollectionView<GameAccount> accountsView = new(accounts) { Filter = gameAccountFilter.Filter };
|
||||
ObservableReorderableDbCollection<GameAccount> accounts = gameService.GameAccountCollection;
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
GameAccountsView = accountsView;
|
||||
GameAccountsView = new(accounts)
|
||||
{
|
||||
Filter = gameAccountFilter.Filter,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,14 +43,13 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
|
||||
protected override async Task LoadAsync()
|
||||
{
|
||||
LaunchScheme? scheme = launchGameShared.GetCurrentLaunchSchemeFromConfigFile();
|
||||
ObservableCollection<GameAccount> accounts = await gameService.GetGameAccountCollectionAsync().ConfigureAwait(false);
|
||||
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
|
||||
|
||||
try
|
||||
{
|
||||
if (scheme is not null)
|
||||
{
|
||||
// Try set to the current account.
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
SelectedGameAccount ??= gameService.DetectCurrentGameAccount(scheme);
|
||||
}
|
||||
}
|
||||
@@ -60,10 +59,12 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
|
||||
}
|
||||
|
||||
gameAccountFilter = new(scheme?.GetSchemeType());
|
||||
AdvancedCollectionView<GameAccount> accountsView = new(accounts) { Filter = gameAccountFilter.Filter };
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
GameAccountsView = accountsView;
|
||||
GameAccountsView = new(accounts)
|
||||
{
|
||||
Filter = gameAccountFilter.Filter,
|
||||
};
|
||||
}
|
||||
|
||||
[Command("LaunchCommand")]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Factory.Picker;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.InterChange.GachaLog;
|
||||
using Snap.Hutao.Service;
|
||||
using Snap.Hutao.Service.GachaLog;
|
||||
|
||||
@@ -85,10 +85,8 @@ internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
AdvancedCollectionView<SpiralAbyssView> spiralAbyssEntries = collection.ToAdvancedCollectionView();
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
SpiralAbyssEntries = spiralAbyssEntries;
|
||||
SpiralAbyssEntries = new(collection);
|
||||
SpiralAbyssEntries.MoveCurrentTo(SpiralAbyssEntries.SourceCollection.FirstOrDefault(s => s.Engaged));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
||||
@@ -72,7 +72,6 @@ internal sealed partial class TitleViewModel : Abstraction.ViewModel
|
||||
{
|
||||
if (LocalSetting.Get(SettingKeys.AlwaysIsFirstRunAfterUpdate, false) || XamlApplicationLifetime.IsFirstRunAfterUpdate)
|
||||
{
|
||||
XamlApplicationLifetime.IsFirstRunAfterUpdate = false;
|
||||
new ShowWebView2WindowAction()
|
||||
{
|
||||
ContentProvider = new UpdateLogContentProvider(),
|
||||
|
||||
@@ -24,6 +24,7 @@ using Snap.Hutao.Web.Response;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using CalculateAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
|
||||
using CalculateBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption;
|
||||
using CalculateClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
|
||||
|
||||
@@ -42,8 +43,8 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
|
||||
private readonly IMetadataService metadataService;
|
||||
private readonly ITaskContext taskContext;
|
||||
private readonly IHutaoSpiralAbyssStatisticsCache hutaoCache;
|
||||
private readonly IServiceScopeFactory serviceScopeFactory;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly CalculateClient calculateClient;
|
||||
private readonly IUserService userService;
|
||||
|
||||
private AdvancedCollectionView<Avatar>? avatars;
|
||||
@@ -108,12 +109,8 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
|
||||
|
||||
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
|
||||
{
|
||||
AdvancedCollectionView<Avatar> avatarsView = list.ToAdvancedCollectionView();
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
Avatars = avatarsView;
|
||||
|
||||
// TODO: use CurrentItem
|
||||
Avatars = new(list);
|
||||
Selected = Avatars.View.ElementAtOrDefault(0);
|
||||
}
|
||||
|
||||
@@ -172,19 +169,16 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
|
||||
|
||||
CalculableOptions options = new(avatar.ToCalculable(), null);
|
||||
CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync<CultivatePromotionDeltaDialog>(options).ConfigureAwait(false);
|
||||
(bool isOk, CultivatePromotionDeltaOptions deltaOptions) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
|
||||
(bool isOk, CalculateAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
|
||||
|
||||
if (!isOk)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Response<CalculateBatchConsumption> response;
|
||||
using (IServiceScope scope = serviceScopeFactory.CreateScope())
|
||||
{
|
||||
CalculateClient calculateClient = scope.ServiceProvider.GetRequiredService<CalculateClient>();
|
||||
response = await calculateClient.BatchComputeAsync(userAndUid, deltaOptions.Delta).ConfigureAwait(false);
|
||||
}
|
||||
Response<CalculateBatchConsumption> response = await calculateClient
|
||||
.BatchComputeAsync(userAndUid, delta)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!response.IsOk())
|
||||
{
|
||||
@@ -192,32 +186,20 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
|
||||
}
|
||||
|
||||
CalculateBatchConsumption batchConsumption = response.Data;
|
||||
LevelInformation levelInformation = LevelInformation.From(deltaOptions.Delta);
|
||||
LevelInformation levelInformation = LevelInformation.From(delta);
|
||||
try
|
||||
{
|
||||
InputConsumption input = new()
|
||||
{
|
||||
Type = CultivateType.AvatarAndSkill,
|
||||
ItemId = avatar.Id,
|
||||
Items = batchConsumption.OverallConsume,
|
||||
LevelInformation = levelInformation,
|
||||
Strategy = deltaOptions.Strategy,
|
||||
};
|
||||
bool saved = await cultivationService
|
||||
.SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, batchConsumption.OverallConsume, levelInformation)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
switch (await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false))
|
||||
if (saved)
|
||||
{
|
||||
case ConsumptionSaveResultKind.NoProject:
|
||||
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
|
||||
break;
|
||||
case ConsumptionSaveResultKind.Skipped:
|
||||
infoBarService.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint);
|
||||
break;
|
||||
case ConsumptionSaveResultKind.NoItem:
|
||||
infoBarService.Information(SH.ViewModelCultivationConsumptionSaveNoItemHint);
|
||||
break;
|
||||
case ConsumptionSaveResultKind.Added:
|
||||
infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
|
||||
}
|
||||
}
|
||||
catch (HutaoException ex)
|
||||
|
||||
@@ -68,12 +68,8 @@ internal sealed partial class WikiMonsterViewModel : Abstraction.ViewModel
|
||||
|
||||
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
|
||||
{
|
||||
AdvancedCollectionView<Monster> monstersView = ordered.ToAdvancedCollectionView();
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
Monsters = monstersView;
|
||||
|
||||
// TODO: use CurrentItem
|
||||
Monsters = new(ordered);
|
||||
Selected = Monsters.View.ElementAtOrDefault(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ using Snap.Hutao.Web.Response;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using CalculateAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
|
||||
using CalculateBatchConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.BatchConsumption;
|
||||
using CalculateClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
|
||||
|
||||
@@ -37,11 +38,11 @@ namespace Snap.Hutao.ViewModel.Wiki;
|
||||
internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
|
||||
{
|
||||
private readonly IContentDialogFactory contentDialogFactory;
|
||||
private readonly CalculateClient calculateClient;
|
||||
private readonly ICultivationService cultivationService;
|
||||
private readonly ITaskContext taskContext;
|
||||
private readonly IMetadataService metadataService;
|
||||
private readonly IHutaoSpiralAbyssStatisticsCache hutaoCache;
|
||||
private readonly IServiceScopeFactory serviceScopeFactory;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly IUserService userService;
|
||||
|
||||
@@ -107,13 +108,9 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
|
||||
|
||||
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
|
||||
{
|
||||
AdvancedCollectionView<Weapon> weaponsView = list.ToAdvancedCollectionView();
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
Weapons = weaponsView;
|
||||
|
||||
// TODO: use CurrentItem
|
||||
Weapons = new(list);
|
||||
Selected = Weapons.View.ElementAtOrDefault(0);
|
||||
}
|
||||
|
||||
@@ -167,19 +164,16 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
|
||||
|
||||
CalculableOptions options = new(null, weapon.ToCalculable());
|
||||
CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync<CultivatePromotionDeltaDialog>(options).ConfigureAwait(false);
|
||||
(bool isOk, CultivatePromotionDeltaOptions deltaOptions) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
|
||||
(bool isOk, CalculateAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
|
||||
|
||||
if (!isOk)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Response<CalculateBatchConsumption> response;
|
||||
using (IServiceScope scope = serviceScopeFactory.CreateScope())
|
||||
{
|
||||
CalculateClient calculateClient = scope.ServiceProvider.GetRequiredService<CalculateClient>();
|
||||
response = await calculateClient.BatchComputeAsync(userAndUid, deltaOptions.Delta).ConfigureAwait(false);
|
||||
}
|
||||
Response<CalculateBatchConsumption> response = await calculateClient
|
||||
.BatchComputeAsync(userAndUid, delta)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!response.IsOk())
|
||||
{
|
||||
@@ -187,32 +181,20 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
|
||||
}
|
||||
|
||||
CalculateBatchConsumption batchConsumption = response.Data;
|
||||
LevelInformation levelInformation = LevelInformation.From(deltaOptions.Delta);
|
||||
LevelInformation levelInformation = LevelInformation.From(delta);
|
||||
try
|
||||
{
|
||||
InputConsumption input = new()
|
||||
{
|
||||
Type = CultivateType.Weapon,
|
||||
ItemId = weapon.Id,
|
||||
Items = batchConsumption.OverallConsume,
|
||||
LevelInformation = levelInformation,
|
||||
Strategy = deltaOptions.Strategy,
|
||||
};
|
||||
bool saved = await cultivationService
|
||||
.SaveConsumptionAsync(CultivateType.Weapon, weapon.Id, batchConsumption.OverallConsume, levelInformation)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
switch (await cultivationService.SaveConsumptionAsync(input).ConfigureAwait(false))
|
||||
if (saved)
|
||||
{
|
||||
case ConsumptionSaveResultKind.NoProject:
|
||||
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
|
||||
break;
|
||||
case ConsumptionSaveResultKind.Skipped:
|
||||
infoBarService.Information(SH.ViewModelCultivationConsumptionSaveSkippedHint);
|
||||
break;
|
||||
case ConsumptionSaveResultKind.NoItem:
|
||||
infoBarService.Information(SH.ViewModelCultivationConsumptionSaveNoItemHint);
|
||||
break;
|
||||
case ConsumptionSaveResultKind.Added:
|
||||
infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
|
||||
}
|
||||
}
|
||||
catch (HutaoException ex)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
|
||||
|
||||
namespace Snap.Hutao.Web;
|
||||
|
||||
@@ -157,6 +158,15 @@ internal static class ApiEndpoints
|
||||
public const string GameRecordRoleCombatPath = $"{ApiTakumiRecordApi}/role_combat";
|
||||
#endregion
|
||||
|
||||
#region ApiTakumiDownloaderApi
|
||||
|
||||
public static string SophonChunkGetBuild(BranchWrapper branch)
|
||||
{
|
||||
return $"{ApiTakumiDownloaderApi}/sophon_chunk/getBuild?branch={branch.Branch}&package_id={branch.PackageId}&password={branch.Password}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ApiTakumiEventCalculate
|
||||
|
||||
#region V1
|
||||
@@ -307,6 +317,11 @@ internal static class ApiEndpoints
|
||||
return $"{HoyoPlayApiConnectApi}/getGameDeprecatedFileConfigs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D}";
|
||||
}
|
||||
|
||||
public static string HoyoPlayConnectGameBranches(LaunchScheme scheme)
|
||||
{
|
||||
return $"{HoyoPlayApiConnectApi}/getGameBranches?game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PassportApi | PassportApiV4
|
||||
@@ -381,6 +396,8 @@ internal static class ApiEndpoints
|
||||
private const string ApiTakumiCardApi = $"{ApiTakumiRecord}/game_record/app/card/api";
|
||||
private const string ApiTakumiCardWApi = $"{ApiTakumiRecord}/game_record/app/card/wapi";
|
||||
|
||||
private const string ApiTakumiDownloaderApi = $"{ApiTakumi}/downloader";
|
||||
|
||||
private const string ApiTakumiEvent = $"{ApiTakumi}/event";
|
||||
private const string ApiTakumiEventCalculate = $"{ApiTakumiEvent}/e20200928calculate";
|
||||
private const string ApiTakumiEventLuna = $"{ApiTakumiEvent}/luna";
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
|
||||
|
||||
namespace Snap.Hutao.Web;
|
||||
|
||||
@@ -254,6 +255,11 @@ internal static class ApiOsEndpoints
|
||||
/// </summary>
|
||||
public const string CalculateSyncAvatarList = $"{SgPublicApi}/event/calculateos/sync/avatar/list";
|
||||
|
||||
public static string SophonChunkGetBuild(BranchWrapper branch)
|
||||
{
|
||||
return $"{SgPublicApi}/sophon_chunk/getBuild?branch={branch.Branch}&package_id={branch.PackageId}&password={branch.Password}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SgHk4eApi
|
||||
@@ -316,6 +322,11 @@ internal static class ApiOsEndpoints
|
||||
return $"{SgHoyoPlayApiConnectApi}/getGameDeprecatedFileConfigs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D}";
|
||||
}
|
||||
|
||||
public static string HoyoPlayConnectGameBranches(LaunchScheme scheme)
|
||||
{
|
||||
return $"{SgHoyoPlayApiConnectApi}/getGameBranches?game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region WebApiOsAccountApi
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
|
||||
|
||||
internal sealed class BranchWrapper
|
||||
{
|
||||
[JsonPropertyName("package_id")]
|
||||
public string PackageId { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("branch")]
|
||||
public string Branch { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("password")]
|
||||
public string Password { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("tag")]
|
||||
public string Tag { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
|
||||
|
||||
internal sealed class GameBranch : GameIndexedObject
|
||||
{
|
||||
[JsonPropertyName("main")]
|
||||
public BranchWrapper Main { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("pre_download")]
|
||||
public BranchWrapper PreDownload { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
|
||||
|
||||
internal sealed class GameBranchesWrapper
|
||||
{
|
||||
[JsonPropertyName("game_branches")]
|
||||
public List<GameBranch> GameBranches { get; set; } = default!;
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
|
||||
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.ChannelSDK;
|
||||
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile;
|
||||
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package;
|
||||
@@ -71,4 +72,21 @@ internal sealed partial class HoyoPlayClient
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
public async ValueTask<Response<GameBranchesWrapper>> GetBranchesAsync(LaunchScheme scheme, CancellationToken token = default)
|
||||
{
|
||||
string url = scheme.IsOversea
|
||||
? ApiOsEndpoints.HoyoPlayConnectGameBranches(scheme)
|
||||
: ApiEndpoints.HoyoPlayConnectGameBranches(scheme);
|
||||
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(url)
|
||||
.Get();
|
||||
|
||||
Response<GameBranchesWrapper>? resp = await builder
|
||||
.SendAsync<Response<GameBranchesWrapper>>(httpClient, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
|
||||
using Snap.Hutao.Web.Response;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
|
||||
|
||||
internal interface ISophonClient
|
||||
{
|
||||
ValueTask<Response<SophonBuild>> GetBuildAsync(BranchWrapper branch, CancellationToken token = default);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
|
||||
|
||||
internal sealed class Manifest
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("checksum")]
|
||||
public string Checksum { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("compressed_size")]
|
||||
public string CompressedSize { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("uncompressed_size")]
|
||||
public string UncompressedSize { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
|
||||
|
||||
internal sealed class ManifestDownloadInfo
|
||||
{
|
||||
[JsonPropertyName("encryption")]
|
||||
public uint Encryption { get; set; }
|
||||
|
||||
[JsonPropertyName("password")]
|
||||
public string Password { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("compression")]
|
||||
public uint Compression { get; set; }
|
||||
|
||||
[JsonPropertyName("url_prefix")]
|
||||
public string UrlPrefix { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("url_suffix")]
|
||||
public string UrlSuffix { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
|
||||
|
||||
internal sealed class ManifestStats
|
||||
{
|
||||
[JsonPropertyName("compressed_size")]
|
||||
public string CompressedSize { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("uncompressed_size")]
|
||||
public string UncompressedSize { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("file_count")]
|
||||
public string FileCount { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("chunk_count")]
|
||||
public string ChunkCount { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
|
||||
|
||||
internal sealed class SophonBuild
|
||||
{
|
||||
[JsonPropertyName("build_id")]
|
||||
public string BuildId { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("tag")]
|
||||
public string Tag { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("manifests")]
|
||||
public List<SophonManifest> Manifests { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
|
||||
|
||||
[ConstructorGenerated(ResolveHttpClient = true)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed partial class SophonClient : ISophonClient
|
||||
{
|
||||
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
|
||||
private readonly ILogger<SophonClient> logger;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
public async ValueTask<Response<SophonBuild>> GetBuildAsync(BranchWrapper branch, CancellationToken token = default)
|
||||
{
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiEndpoints.SophonChunkGetBuild(branch))
|
||||
.Get();
|
||||
|
||||
Response<SophonBuild>? resp = await builder
|
||||
.SendAsync<Response<SophonBuild>>(httpClient, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
|
||||
|
||||
[Injection(InjectAs.Transient, typeof(IOverseaSupportFactory<ISophonClient>))]
|
||||
[ConstructorGenerated(CallBaseConstructor = true)]
|
||||
internal sealed partial class SophonClientFactory : OverseaSupportFactory<ISophonClient, SophonClient, SophonClientOversea>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Branch;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
|
||||
|
||||
[ConstructorGenerated(ResolveHttpClient = true)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed partial class SophonClientOversea : ISophonClient
|
||||
{
|
||||
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
|
||||
private readonly ILogger<SophonClientOversea> logger;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
public async ValueTask<Response<SophonBuild>> GetBuildAsync(BranchWrapper branch, CancellationToken token = default)
|
||||
{
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiOsEndpoints.SophonChunkGetBuild(branch))
|
||||
.Get();
|
||||
|
||||
Response<SophonBuild>? resp = await builder
|
||||
.SendAsync<Response<SophonBuild>>(httpClient, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Downloader;
|
||||
|
||||
internal sealed class SophonManifest
|
||||
{
|
||||
[JsonPropertyName("category_id")]
|
||||
public string CategoryId { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("category_name")]
|
||||
public string CategoryName { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("manifest")]
|
||||
public Manifest Manifest { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("chunk_download")]
|
||||
public ManifestDownloadInfo ChunkDownload { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("manifest_download")]
|
||||
public ManifestDownloadInfo ManifestDownload { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("matching_field")]
|
||||
public string MatchingField { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("stats")]
|
||||
public ManifestStats Stats { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("deduplicated_stats")]
|
||||
public ManifestStats DeduplicatedStats { get; set; } = default!;
|
||||
}
|
||||
@@ -228,7 +228,7 @@ internal sealed partial class GameRecordClient : IGameRecordClient
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// We have a verification procedure to handle
|
||||
if (resp?.ReturnCode is (int)KnownReturnCode.CODE1034)
|
||||
if (resp?.ReturnCode == (int)KnownReturnCode.CODE1034)
|
||||
{
|
||||
// Replace message
|
||||
resp.Message = SH.WebIndexOrSpiralAbyssVerificationFailed;
|
||||
|
||||
@@ -8,7 +8,7 @@ using System.Net.Http.Headers;
|
||||
|
||||
namespace Snap.Hutao.Web.Request.Builder;
|
||||
|
||||
internal sealed class HttpRequestMessageBuilder :
|
||||
internal class HttpRequestMessageBuilder :
|
||||
IBuilder,
|
||||
IHttpRequestMessageBuilder,
|
||||
IHttpHeadersBuilder<HttpHeaders>,
|
||||
@@ -22,14 +22,12 @@ internal sealed class HttpRequestMessageBuilder :
|
||||
IHttpMethodBuilder
|
||||
{
|
||||
private readonly HttpContentSerializer httpContentSerializer;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private HttpRequestMessage httpRequestMessage;
|
||||
|
||||
public HttpRequestMessageBuilder(IServiceProvider serviceProvider, HttpContentSerializer httpContentSerializer, HttpRequestMessage? httpRequestMessage = default)
|
||||
public HttpRequestMessageBuilder(HttpContentSerializer httpContentSerializer, HttpRequestMessage? httpRequestMessage = default)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
this.httpContentSerializer = httpContentSerializer;
|
||||
this.httpRequestMessage = httpRequestMessage ?? new();
|
||||
this.httpRequestMessage = httpRequestMessage ?? new HttpRequestMessage();
|
||||
}
|
||||
|
||||
public HttpRequestMessage HttpRequestMessage
|
||||
@@ -42,8 +40,6 @@ internal sealed class HttpRequestMessageBuilder :
|
||||
}
|
||||
}
|
||||
|
||||
public IServiceProvider ServiceProvider { get => serviceProvider; }
|
||||
|
||||
public HttpContentSerializer HttpContentSerializer { get => httpContentSerializer; }
|
||||
|
||||
HttpContentHeaders IHttpHeadersBuilder<HttpContentHeaders>.Headers
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using Snap.Hutao.Web.Hutao.Response;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Web.Request.Builder;
|
||||
|
||||
@@ -25,10 +22,6 @@ internal static class HttpRequestMessageBuilderExtension
|
||||
internal static async ValueTask<TResult?> SendAsync<TResult>(this HttpRequestMessageBuilder builder, HttpClient httpClient, ILogger logger, CancellationToken token)
|
||||
where TResult : class
|
||||
{
|
||||
StringBuilder messageBuilder = new();
|
||||
messageBuilder.AppendLine(System.Globalization.CultureInfo.CurrentCulture, $"Host: {builder.RequestUri?.Host}");
|
||||
bool showInfo = true;
|
||||
|
||||
try
|
||||
{
|
||||
using (builder.HttpRequestMessage)
|
||||
@@ -36,51 +29,52 @@ internal static class HttpRequestMessageBuilderExtension
|
||||
using (HttpResponseMessage message = await httpClient.SendAsync(builder.HttpRequestMessage, token).ConfigureAwait(false))
|
||||
{
|
||||
message.EnsureSuccessStatusCode();
|
||||
showInfo = false;
|
||||
return await builder.HttpContentSerializer.DeserializeAsync<TResult>(message.Content, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
if (TryHandleHttp502HutaoResponseSpecialCase(ex, out TResult? result))
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
|
||||
|
||||
if (ex.StatusCode is HttpStatusCode.BadGateway)
|
||||
{
|
||||
return result;
|
||||
Type resultType = typeof(TResult);
|
||||
|
||||
if (resultType == typeof(HutaoResponse))
|
||||
{
|
||||
return Activator.CreateInstance(resultType, 502, SH.WebHutaoServiceUnAvailable, default) as TResult;
|
||||
}
|
||||
|
||||
ProcessException(messageBuilder, ex);
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
|
||||
}
|
||||
catch (IOException ex)
|
||||
if (resultType.IsConstructedGenericType && resultType.GetGenericTypeDefinition() == typeof(HutaoResponse<>))
|
||||
{
|
||||
ProcessException(messageBuilder, ex);
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
ProcessException(messageBuilder, ex);
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
|
||||
}
|
||||
catch (HttpContentSerializationException ex)
|
||||
{
|
||||
ProcessException(messageBuilder, ex);
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
ProcessException(messageBuilder, ex);
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (showInfo)
|
||||
{
|
||||
builder.ServiceProvider.GetRequiredService<IInfoBarService>().Error(messageBuilder.ToString());
|
||||
return Activator.CreateInstance(resultType, 502, SH.WebHutaoServiceUnAvailable, default, default) as TResult;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
|
||||
return default;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
|
||||
return default;
|
||||
}
|
||||
catch (HttpContentSerializationException ex)
|
||||
{
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
|
||||
return default;
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Send(this HttpRequestMessageBuilder builder, HttpClient httpClient, ILogger logger)
|
||||
{
|
||||
@@ -92,95 +86,23 @@ internal static class HttpRequestMessageBuilderExtension
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
|
||||
}
|
||||
catch (HttpContentSerializationException ex)
|
||||
{
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryHandleHttp502HutaoResponseSpecialCase<TResult>(HttpRequestException ex, out TResult? result)
|
||||
where TResult : class
|
||||
{
|
||||
result = default;
|
||||
|
||||
if (ex.StatusCode is HttpStatusCode.BadGateway)
|
||||
{
|
||||
Type resultType = typeof(TResult);
|
||||
|
||||
if (resultType == typeof(HutaoResponse))
|
||||
{
|
||||
// HutaoResponse(int returnCode, string message, string? localizationKey)
|
||||
result = Activator.CreateInstance(resultType, 502, SH.WebHutaoServiceUnAvailable, default) as TResult;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (resultType.IsConstructedGenericType && resultType.GetGenericTypeDefinition() == typeof(HutaoResponse<>))
|
||||
{
|
||||
// HutaoResponse<TData>(int returnCode, string message, TData? data, string? localizationKey)
|
||||
result = Activator.CreateInstance(resultType, 502, SH.WebHutaoServiceUnAvailable, default, default) as TResult;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[SuppressMessage("", "CA1305")]
|
||||
private static void ProcessException(StringBuilder builder, Exception exception)
|
||||
{
|
||||
if (exception is HttpRequestException hre)
|
||||
{
|
||||
builder
|
||||
.AppendLine($"{nameof(HttpRequestException)}: Status Code: {hre.StatusCode} Error: {hre.HttpRequestError}")
|
||||
.AppendLine(hre.Message);
|
||||
}
|
||||
|
||||
if (exception is IOException ioe)
|
||||
{
|
||||
builder
|
||||
.AppendLine($"{nameof(IOException)}: 0x{ioe.HResult:X8}")
|
||||
.AppendLine(ioe.Message);
|
||||
}
|
||||
|
||||
if (exception is JsonException je)
|
||||
{
|
||||
builder
|
||||
.AppendLine($"{nameof(JsonException)}: Path: {je.Path} at Line: {je.LineNumber} Position: {je.BytePositionInLine}")
|
||||
.AppendLine(je.Message);
|
||||
}
|
||||
|
||||
if (exception is HttpContentSerializationException hcse)
|
||||
{
|
||||
builder
|
||||
.AppendLine($"{nameof(HttpContentSerializationException)}:")
|
||||
.AppendLine(hcse.Message);
|
||||
}
|
||||
|
||||
if (exception is SocketException se)
|
||||
{
|
||||
builder
|
||||
.AppendLine($"{nameof(SocketException)}: Error: {se.SocketErrorCode}")
|
||||
.AppendLine(se.Message);
|
||||
}
|
||||
|
||||
if (exception.InnerException is { } inner)
|
||||
{
|
||||
builder.AppendLine(new string('-', 40));
|
||||
ProcessException(builder, inner);
|
||||
logger.LogWarning(ex, RequestErrorMessage, builder.HttpRequestMessage.RequestUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,9 @@ namespace Snap.Hutao.Web.Request.Builder;
|
||||
internal sealed partial class HttpRequestMessageBuilderFactory : IHttpRequestMessageBuilderFactory
|
||||
{
|
||||
private readonly JsonHttpContentSerializer jsonHttpContentSerializer;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
public HttpRequestMessageBuilder Create()
|
||||
{
|
||||
return new(serviceProvider, jsonHttpContentSerializer);
|
||||
return new(jsonHttpContentSerializer);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user