diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs index a6519389..6449ff8d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs @@ -23,6 +23,11 @@ internal static class Activation /// public const string LaunchGame = "LaunchGame"; + /// + /// 从剪贴板导入成就 + /// + public const string ImportUIAFFromClipBoard = "ImportUIAFFromClipBoard"; + private static readonly SemaphoreSlim ActivateSemaphore = new(1); /// @@ -180,7 +185,7 @@ internal static class Activation { await ThreadHelper.SwitchToMainThreadAsync(); - INavigationAwaiter navigationAwaiter = new NavigationExtra("InvokeByUri"); + INavigationAwaiter navigationAwaiter = new NavigationExtra(ImportUIAFFromClipBoard); await Ioc.Default .GetRequiredService() .NavigateAsync(navigationAwaiter, true) diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Cultivation/CultivateItem.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Cultivation/CultivateItem.cs index 74d56fca..44b5c7fa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Cultivation/CultivateItem.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Cultivation/CultivateItem.cs @@ -65,6 +65,11 @@ public class CultivateItem : ObservableObject /// public bool IsToday { get; } + /// + /// 对应背包物品的个数 + /// + public uint InventoryItemCount { get; set; } + private void FlipIsFinished() { IsFinished = !IsFinished; diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Cultivation/StatisticsCultivateItem.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Cultivation/StatisticsCultivateItem.cs new file mode 100644 index 00000000..e4ad85f8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Cultivation/StatisticsCultivateItem.cs @@ -0,0 +1,51 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Snap.Hutao.Model.Metadata; +using Snap.Hutao.Service.Cultivation; + +namespace Snap.Hutao.Model.Binding.Cultivation; + +/// +/// 仅用于统计总数的养成物品 +/// +public class StatisticsCultivateItem +{ + /// + /// 构造一个新的统计用养成物品 + /// + /// 材料 + /// 实体 + public StatisticsCultivateItem(Material inner, Entity.CultivateItem entity) + { + Inner = inner; + Count = entity.Count; + } + + /// + /// 元数据 + /// + public Material Inner { get; } + + /// + /// 对应背包物品的个数 + /// + public int Count { get; set; } + + /// + /// 对应背包物品的个数 + /// + public uint TotalCount { get; set; } + + /// + /// 是否完成 + /// + public bool IsFinished { get => Count >= TotalCount; } + + /// + /// 格式化总数 + /// + public string CountFormatted { get => $"{Count}/{TotalCount}"; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/Team.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/Team.cs index 3d94fe4f..672cf831 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/Team.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/Team.cs @@ -28,8 +28,14 @@ internal class Team : List } Rate = $"上场 {team.Rate} 次"; + Name = TeamPopularNameParser.GetName(ids.ToHashSet()); } + /// + /// 队伍俗名 + /// + public string Name { get; set; } + /// /// 上场次数 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/TeamPopularNameParser.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/TeamPopularNameParser.cs new file mode 100644 index 00000000..a598a776 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/Hutao/TeamPopularNameParser.cs @@ -0,0 +1,51 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Metadata; +using System.Collections.Immutable; + +namespace Snap.Hutao.Model.Binding.Hutao; + +/// +/// 队伍俗名解析器 +/// +internal static class TeamPopularNameParser +{ + /// + /// 已知的队伍名称 + /// + private static readonly ImmutableDictionary> KnownTeamNames = new Dictionary>() + { + ["雷国|雷行香班"] = new HashSet() { AvatarIds.Shougun, AvatarIds.Xingqiu, AvatarIds.Xiangling, AvatarIds.Bennett, }.ToImmutableHashSet(), + ["雷国|雷夜香班"] = new HashSet() { AvatarIds.Shougun, AvatarIds.Yelan, AvatarIds.Xiangling, AvatarIds.Bennett, }.ToImmutableHashSet(), + ["雷国|雷万香班"] = new HashSet() { AvatarIds.Shougun, AvatarIds.Kazuha, AvatarIds.Xiangling, AvatarIds.Bennett, }.ToImmutableHashSet(), + ["雷九|雷九万班"] = new HashSet() { AvatarIds.Shougun, AvatarIds.Sara, AvatarIds.Kazuha, AvatarIds.Bennett, }.ToImmutableHashSet(), + ["万达国际"] = new HashSet() { AvatarIds.Kazuha, AvatarIds.Tartaglia, AvatarIds.Xiangling, AvatarIds.Bennett, }.ToImmutableHashSet(), + ["胡行钟夜"] = new HashSet() { AvatarIds.Hutao, AvatarIds.Xingqiu, AvatarIds.Zhongli, AvatarIds.Yelan, }.ToImmutableHashSet(), + ["激晴|刻皇万妲"] = new HashSet() { AvatarIds.Keqing, AvatarIds.Fischl, AvatarIds.Kazuha, AvatarIds.Nahida, }.ToImmutableHashSet(), + ["永冻|神鹤万心"] = new HashSet() { AvatarIds.Ayaka, AvatarIds.Shenhe, AvatarIds.Kazuha, AvatarIds.Kokomi, }.ToImmutableHashSet(), + ["永冻|神钟万心"] = new HashSet() { AvatarIds.Ayaka, AvatarIds.Zhongli, AvatarIds.Kazuha, AvatarIds.Kokomi, }.ToImmutableHashSet(), + ["融甘|融化甘雨"] = new HashSet() { AvatarIds.Ganyu, AvatarIds.Zhongli, AvatarIds.Xiangling, AvatarIds.Bennett, }.ToImmutableHashSet(), + ["妮绽放|女主"] = new HashSet() { AvatarIds.Nilou, AvatarIds.Kokomi, AvatarIds.Nahida, AvatarIds.PlayerGirl, }.ToImmutableHashSet(), + ["妮绽放|男主"] = new HashSet() { AvatarIds.Nilou, AvatarIds.Kokomi, AvatarIds.Nahida, AvatarIds.PlayerBoy, }.ToImmutableHashSet(), + ["一斗岩队"] = new HashSet() { AvatarIds.Itto, AvatarIds.Albedo, AvatarIds.Zhongli, AvatarIds.Gorou, }.ToImmutableHashSet(), + }.ToImmutableDictionary(); + + /// + /// 获取队伍名称 + /// + /// 队伍集 + /// 队伍名称 + public static string GetName(HashSet ids) + { + foreach (KeyValuePair> entry in KnownTeamNames) + { + if (entry.Value.SetEquals(ids)) + { + return entry.Key; + } + } + + return "尚未命名"; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/WeaponType.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/WeaponType.cs index a0e594d0..da1e0de6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/WeaponType.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/WeaponType.cs @@ -74,24 +74,24 @@ public enum WeaponType /// /// 法器 /// - [Description("单手剑")] + [Description("法器")] WEAPON_CATALYST = 10, /// /// 双手剑 /// - [Description("单手剑")] + [Description("双手剑")] WEAPON_CLAYMORE = 11, /// /// 弓 /// - [Description("单手剑")] + [Description("弓")] WEAPON_BOW = 12, /// /// 长柄武器 /// - [Description("单手剑")] + [Description("长柄武器")] WEAPON_POLE = 13, } diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest index fd6073a2..a0adddf2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest +++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest @@ -12,7 +12,7 @@ + Version="1.2.19.0" /> 胡桃 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs index 98a2c640..f26cf7c0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs @@ -22,6 +22,7 @@ namespace Snap.Hutao.Service.Achievement; [Injection(InjectAs.Scoped, typeof(IAchievementService))] internal class AchievementService : IAchievementService { + private readonly object saveAchievementLocker = new(); private readonly AppDbContext appDbContext; private readonly ILogger logger; private readonly DbCurrent dbCurrent; @@ -187,4 +188,19 @@ internal class AchievementService : IAchievementService logger.LogInformation(EventIds.Achievement, "{add} added, {update} updated, {remove} removed", result.Add, result.Update, result.Remove); logger.LogInformation(EventIds.Achievement, "Save achievements for [{name}] completed in {time}ms", name, time); } + + /// + public void SaveAchievement(BindingAchievement achievement) + { + if (achievement.IsChecked) + { + // set to default allow multiple time add + achievement.Entity.InnerId = default; + appDbContext.Achievements.UpdateAndSave(achievement.Entity); + } + else + { + appDbContext.Achievements.RemoveAndSave(achievement.Entity); + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs index a2bae36f..45da7456 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/IAchievementService.cs @@ -1,7 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Core.Threading.CodeAnalysis; using Snap.Hutao.Model.InterChange.Achievement; using System.Collections.ObjectModel; using BindingAchievement = Snap.Hutao.Model.Binding.Achievement.Achievement; @@ -57,6 +56,12 @@ internal interface IAchievementService /// 任务 Task RemoveArchiveAsync(EntityArchive archive); + /// + /// 保存单个成就 + /// + /// 成就 + void SaveAchievement(BindingAchievement achievement); + /// /// 保存成就 /// @@ -69,6 +74,5 @@ internal interface IAchievementService /// /// 新存档 /// 存档添加结果 - [ThreadAccess(ThreadAccessState.AnyThread)] Task TryAddArchiveAsync(EntityArchive newArchive); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs index 95bb831f..25b48ad3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs @@ -148,7 +148,7 @@ internal class CultivationService : ICultivationService .ToListAsync() .ConfigureAwait(false); - foreach (CultivateEntry? entry in entries) + foreach (CultivateEntry entry in entries) { Guid entryId = entry.InnerId; @@ -178,6 +178,70 @@ internal class CultivationService : ICultivationService } } + /// + public async Task> GetStatisticsCultivateItemsAsync(CultivateProject cultivateProject, List materials) + { + using (IServiceScope scope = scopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + + Guid projectId = cultivateProject.InnerId; + + List resultItems = new(); + + List entries = await appDbContext.CultivateEntries + .AsNoTracking() + .Where(e => e.ProjectId == projectId) + .ToListAsync() + .ConfigureAwait(false); + + foreach (CultivateEntry entry in entries) + { + Guid entryId = entry.InnerId; + + List items = await appDbContext.CultivateItems + .AsNoTracking() + .Where(i => i.EntryId == entryId) + .OrderBy(i => i.ItemId) + .ToListAsync() + .ConfigureAwait(false); + + foreach (CultivateItem item in items) + { + if (item.IsFinished) + { + continue; + } + + if (resultItems.SingleOrDefault(i => i.Inner.Id == item.ItemId) is Model.Binding.Cultivation.StatisticsCultivateItem inPlaceItem) + { + inPlaceItem.Count += item.Count; + } + else + { + resultItems.Add(new(materials.Single(m => m.Id == item.ItemId), item)); + } + } + } + + List inventoryItems = await appDbContext.InventoryItems + .AsNoTracking() + .Where(e => e.ProjectId == projectId) + .ToListAsync() + .ConfigureAwait(false); + + foreach (InventoryItem inventoryItem in inventoryItems) + { + if (resultItems.SingleOrDefault(i => i.Inner.Id == inventoryItem.ItemId) is Model.Binding.Cultivation.StatisticsCultivateItem inPlaceItem) + { + inPlaceItem.TotalCount += inventoryItem.Count; + } + } + + return resultItems.OrderByDescending(i => i.Count).ToList(); + } + } + /// public async Task RemoveCultivateEntryAsync(Guid entryId) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs index f6e12b88..dd912efc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs @@ -44,6 +44,14 @@ internal interface ICultivationService /// 项目集合 ObservableCollection GetProjectCollection(); + /// + /// 异步获取统计物品列表 + /// + /// 养成计划 + /// 元数据 + /// 统计物品列表 + Task> GetStatisticsCultivateItemsAsync(CultivateProject cultivateProject, List materials); + /// /// 删除养成清单 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs index a4167f77..34e9e1dd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs @@ -244,7 +244,7 @@ internal class GameService : IGameService, IDisposable string commandLine = new CommandLineBuilder() .AppendIf("-popupwindow", configuration.IsBorderless) .Append("-screen-fullscreen", configuration.IsFullScreen ? 1 : 0) - .AppendIfNotNull("-window-mode", configuration.WindowMode) + .AppendIf("-window-mode", configuration.IsExclusive, "exclusive") .Append("-screen-width", configuration.ScreenWidth) .Append("-screen-height", configuration.ScreenHeight) .ToString(); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchConfiguration.cs index f203d0b9..c52a5cc0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchConfiguration.cs @@ -8,6 +8,11 @@ namespace Snap.Hutao.Service.Game; /// internal readonly struct LaunchConfiguration { + /// + /// 是否为独占全屏 + /// + public readonly bool IsExclusive; + /// /// 是否全屏,全屏时无边框设置将被覆盖 /// @@ -41,14 +46,16 @@ internal readonly struct LaunchConfiguration /// /// 构造一个新的启动配置 /// + /// 独占全屏 /// 全屏 /// 无边框 /// 宽度 /// 高度 /// 解锁帧率 /// 目标帧率 - public LaunchConfiguration(bool isFullScreen, bool isBorderless, int screenWidth, int screenHeight, bool unlockFps, int targetFps) + public LaunchConfiguration(bool isExclusive, bool isFullScreen, bool isBorderless, int screenWidth, int screenHeight, bool unlockFps, int targetFps) { + IsExclusive = isExclusive; IsFullScreen = isFullScreen; IsBorderless = isBorderless; ScreenHeight = screenHeight; @@ -57,15 +64,4 @@ internal readonly struct LaunchConfiguration UnlockFPS = unlockFps; TargetFPS = targetFps; } - - /// - /// 窗口模式字符串 - /// - public string? WindowMode - { - get - { - return IsFullScreen ? "exclusive" : IsBorderless ? "borderless" : null; - } - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 9b3d8d0b..eb65dd03 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -139,7 +139,7 @@ - + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml index d9db0832..89dc62c4 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml @@ -15,6 +15,10 @@ Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" mc:Ignorable="d"> + + + + @@ -77,12 +81,8 @@ Command="{Binding RemoveArchiveCommand}" Icon="{shcm:FontIcon Glyph=}" Label="删除当前存档"/> - - + @@ -102,6 +102,7 @@ Icon="{shcm:FontIcon Glyph=}" Label="导出"/> + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml index c8f788b9..037c1923 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml @@ -49,6 +49,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -97,6 +139,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -231,117 +306,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index acbb837a..a33e2da5 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -158,6 +158,17 @@ + + + + + [Injection(InjectAs.Scoped)] +[SuppressMessage("", "SA1124")] internal class AchievementViewModel : ObservableObject, ISupportCancellation, - INavigationRecipient, - IDisposable, - IRecipient + INavigationRecipient { private static readonly SortDescription IncompletedItemsFirstSortDescription = new(nameof(Model.Binding.Achievement.Achievement.IsChecked), SortDirection.Ascending); private static readonly SortDescription CompletionTimeSortDescription = new(nameof(Model.Binding.Achievement.Achievement.Time), SortDirection.Descending); + private readonly IAchievementService achievementService; private readonly IMetadataService metadataService; private readonly IInfoBarService infoBarService; private readonly JsonSerializerOptions options; - private readonly IAchievementService achievementService; - private readonly TaskCompletionSource openUICompletionSource = new(); - private bool disposed; - private AdvancedCollectionView? achievements; private List? achievementGoals; private Model.Binding.Achievement.AchievementGoal? selectedAchievementGoal; @@ -94,9 +91,7 @@ internal class AchievementViewModel RemoveArchiveCommand = asyncRelayCommandFactory.Create(RemoveArchiveAsync); SearchAchievementCommand = new RelayCommand(SearchAchievement); SortIncompletedSwitchCommand = new RelayCommand(UpdateAchievementsSort); - RefreshFinishPercentCommand = new RelayCommand(UpdateAchievementFinishPercent); - - messenger.Register(this); + SaveAchievementCommand = new RelayCommand(SaveAchievement); } /// @@ -127,6 +122,10 @@ internal class AchievementViewModel if (SetProperty(ref selectedArchive, value)) { achievementService.CurrentArchive = value; + if (value != null) + { + UpdateAchievementsAsync(value).SafeForget(); + } } } } @@ -159,7 +158,7 @@ internal class AchievementViewModel { SetProperty(ref selectedAchievementGoal, value); SearchText = string.Empty; - UpdateAchievementFilter(value); + UpdateAchievementsFilter(value); } } @@ -227,36 +226,16 @@ internal class AchievementViewModel public ICommand SortIncompletedSwitchCommand { get; } /// - /// 刷新完成百分比命令 + /// 保存单个成就命令 /// - public ICommand RefreshFinishPercentCommand { get; } - - /// - public void Receive(AchievementArchiveChangedMessage message) - { - HandleArchiveChangeAsync(message.OldValue, message.NewValue).SafeForget(); - } - - /// - public void Dispose() - { - if (!disposed) - { - if (Achievements != null && SelectedArchive != null) - { - achievementService.SaveAchievements(SelectedArchive, (Achievements.Source as IList)!); - } - - disposed = true; - } - } + public ICommand SaveAchievementCommand { get; } /// public async Task ReceiveAsync(INavigationData data) { if (await openUICompletionSource.Task.ConfigureAwait(false)) { - if (data.Data is "InvokeByUri") + if (data.Data is Activation.ImportUIAFFromClipBoard) { await ImportUIAFFromClipboardAsync().ConfigureAwait(false); return true; @@ -268,43 +247,32 @@ internal class AchievementViewModel private static Task ShowImportResultDialogAsync(string title, string message) { + MainWindow mainWindow = Ioc.Default.GetRequiredService(); ContentDialog dialog = new() { Title = title, Content = message, PrimaryButtonText = "确认", DefaultButton = ContentDialogButton.Primary, + XamlRoot = mainWindow.Content.XamlRoot, }; - MainWindow mainWindow = Ioc.Default.GetRequiredService(); - return dialog.InitializeWithWindow(mainWindow).ShowAsync().AsTask(); + return dialog.ShowAsync().AsTask(); } private static Task ShowImportFailDialogAsync(string message) { + MainWindow mainWindow = Ioc.Default.GetRequiredService(); ContentDialog dialog = new() { Title = "导入失败", Content = message, PrimaryButtonText = "确认", DefaultButton = ContentDialogButton.Primary, + XamlRoot = mainWindow.Content.XamlRoot, }; - MainWindow mainWindow = Ioc.Default.GetRequiredService(); - return dialog.InitializeWithWindow(mainWindow).ShowAsync().AsTask(); - } - - private async Task HandleArchiveChangeAsync(Model.Entity.AchievementArchive? oldArchieve, Model.Entity.AchievementArchive? newArchieve) - { - if (oldArchieve != null && Achievements != null) - { - achievementService.SaveAchievements(oldArchieve, (Achievements.Source as IList)!); - } - - if (newArchieve != null) - { - await UpdateAchievementsAsync(newArchieve).ConfigureAwait(false); - } + return dialog.ShowAsync().AsTask(); } private async Task OpenUIAsync() @@ -339,20 +307,7 @@ internal class AchievementViewModel IsInitialized = true; } - private async Task UpdateAchievementsAsync(Model.Entity.AchievementArchive archive) - { - List rawAchievements = await metadataService.GetAchievementsAsync(CancellationToken).ConfigureAwait(false); - List combined = achievementService.GetAchievements(archive, rawAchievements); - - // Assemble achievements on the UI thread. - await ThreadHelper.SwitchToMainThreadAsync(); - Achievements = new(combined, true); - - UpdateAchievementFinishPercent(); - UpdateAchievementFilter(SelectedAchievementGoal); - UpdateAchievementsSort(); - } - + #region 存档操作 private async Task AddArchiveAsync() { MainWindow mainWindow = Ioc.Default.GetRequiredService(); @@ -404,7 +359,33 @@ internal class AchievementViewModel } } } + #endregion + private void SearchAchievement(string? search) + { + if (Achievements != null) + { + SetProperty(ref selectedAchievementGoal, null); + + if (!string.IsNullOrEmpty(search)) + { + if (search.Length == 5 && int.TryParse(search, out int achiId)) + { + Achievements.Filter = (object o) => ((Model.Binding.Achievement.Achievement)o).Inner.Id == achiId; + } + else + { + Achievements.Filter = (object o) => + { + Model.Binding.Achievement.Achievement achi = (Model.Binding.Achievement.Achievement)o; + return achi.Inner.Title.Contains(search) || achi.Inner.Description.Contains(search); + }; + } + } + } + } + + #region 导入导出 private async Task ExportAsUIAFToFileAsync() { if (SelectedArchive == null || Achievements == null) @@ -413,8 +394,6 @@ internal class AchievementViewModel return; } - achievementService.SaveAchievements(SelectedArchive, (Achievements.Source as IList)!); - await ThreadHelper.SwitchToMainThreadAsync(); IPickerFactory pickerFactory = Ioc.Default.GetRequiredService(); FileSavePicker picker = pickerFactory.GetFileSavePicker(); @@ -440,30 +419,6 @@ internal class AchievementViewModel } } - private void SearchAchievement(string? search) - { - if (Achievements != null) - { - SetProperty(ref selectedAchievementGoal, null); - - if (!string.IsNullOrEmpty(search)) - { - if (search.Length == 5 && int.TryParse(search, out int achiId)) - { - Achievements.Filter = (object o) => ((Model.Binding.Achievement.Achievement)o).Inner.Id == achiId; - } - else - { - Achievements.Filter = (object o) => - { - Model.Binding.Achievement.Achievement achi = (Model.Binding.Achievement.Achievement)o; - return achi.Inner.Title.Contains(search) || achi.Inner.Description.Contains(search); - }; - } - } - } - } - private async Task ImportUIAFFromClipboardAsync() { if (achievementService.CurrentArchive == null) @@ -559,6 +514,21 @@ internal class AchievementViewModel return false; } + #endregion + + private async Task UpdateAchievementsAsync(Model.Entity.AchievementArchive archive) + { + List rawAchievements = await metadataService.GetAchievementsAsync(CancellationToken).ConfigureAwait(false); + List combined = achievementService.GetAchievements(archive, rawAchievements); + + // Assemble achievements on the UI thread. + await ThreadHelper.SwitchToMainThreadAsync(); + Achievements = new(combined, true); + + UpdateAchievementsFinishPercent(); + UpdateAchievementsFilter(SelectedAchievementGoal); + UpdateAchievementsSort(); + } private void UpdateAchievementsSort() { @@ -576,7 +546,7 @@ internal class AchievementViewModel } } - private void UpdateAchievementFilter(Model.Binding.Achievement.AchievementGoal? goal) + private void UpdateAchievementsFilter(Model.Binding.Achievement.AchievementGoal? goal) { if (Achievements != null) { @@ -586,16 +556,16 @@ internal class AchievementViewModel } } - private void UpdateAchievementFinishPercent() + private void UpdateAchievementsFinishPercent() { int finished = 0; int count = 0; if (Achievements != null && AchievementGoals != null) { - Dictionary counter = AchievementGoals.ToDictionary(x => x.Id, x => new AchievementGoalAggregation(x)); - foreach (Model.Binding.Achievement.Achievement achievement in Achievements.OfType()) + Dictionary counter = AchievementGoals.ToDictionary(x => x.Id, x => new GoalAggregation(x)); + foreach (Model.Binding.Achievement.Achievement achievement in Achievements.SourceCollection.OfType()) { - ref AchievementGoalAggregation aggregation = ref CollectionsMarshal.GetValueRefOrNullRef(counter, achievement.Inner.Goal); + ref GoalAggregation aggregation = ref CollectionsMarshal.GetValueRefOrNullRef(counter, achievement.Inner.Goal); aggregation.Count += 1; count += 1; if (achievement.IsChecked) @@ -605,7 +575,7 @@ internal class AchievementViewModel } } - foreach (AchievementGoalAggregation aggregation1 in counter.Values) + foreach (GoalAggregation aggregation1 in counter.Values) { aggregation1.AchievementGoal.UpdateFinishPercent(aggregation1.Finished, aggregation1.Count); } @@ -614,13 +584,22 @@ internal class AchievementViewModel } } - private struct AchievementGoalAggregation + private void SaveAchievement(Model.Binding.Achievement.Achievement? achievement) + { + if (achievement != null) + { + achievementService.SaveAchievement(achievement); + UpdateAchievementsFinishPercent(); + } + } + + private struct GoalAggregation { public readonly Model.Binding.Achievement.AchievementGoal AchievementGoal; public int Finished; public int Count; - public AchievementGoalAggregation(Model.Binding.Achievement.AchievementGoal goal) + public GoalAggregation(Model.Binding.Achievement.AchievementGoal goal) { AchievementGoal = goal; } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/CultivationViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/CultivationViewModel.cs index aae44fe2..0e00aa91 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/CultivationViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/CultivationViewModel.cs @@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.Messaging; using Snap.Hutao.Control; using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Message; +using Snap.Hutao.Model.Binding.Cultivation; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Cultivation; @@ -20,7 +21,7 @@ namespace Snap.Hutao.ViewModel; /// 养成视图模型 /// [Injection(InjectAs.Scoped)] -internal class CultivationViewModel : ObservableObject, ISupportCancellation, IRecipient +internal class CultivationViewModel : ObservableObject, ISupportCancellation { private readonly ICultivationService cultivationService; private readonly IInfoBarService infoBarService; @@ -31,6 +32,7 @@ internal class CultivationViewModel : ObservableObject, ISupportCancellation, IR private CultivateProject? selectedProject; private List? inventoryItems; private ObservableCollection? cultivateEntries; + private List? statisticsItems; /// /// 构造一个新的养成视图模型 @@ -40,14 +42,12 @@ internal class CultivationViewModel : ObservableObject, ISupportCancellation, IR /// 异步命令工厂 /// 元数据服务 /// 日志器 - /// 消息器 public CultivationViewModel( ICultivationService cultivationService, IInfoBarService infoBarService, IAsyncRelayCommandFactory asyncRelayCommandFactory, IMetadataService metadataService, - ILogger logger, - IMessenger messenger) + ILogger logger) { this.cultivationService = cultivationService; this.infoBarService = infoBarService; @@ -59,8 +59,7 @@ internal class CultivationViewModel : ObservableObject, ISupportCancellation, IR RemoveProjectCommand = asyncRelayCommandFactory.Create(RemoveProjectAsync); RemoveEntryCommand = asyncRelayCommandFactory.Create(RemoveEntryAsync); SaveInventoryItemCommand = new RelayCommand(SaveInventoryItem); - - messenger.Register(this); + UpdateStatisticsItemsCommand = asyncRelayCommandFactory.Create(UpdateStatisticsItemsAsync); } /// @@ -81,6 +80,10 @@ internal class CultivationViewModel : ObservableObject, ISupportCancellation, IR if (SetProperty(ref selectedProject, value)) { cultivationService.Current = value; + if (value != null) + { + UpdateCultivateEntriesAndInventoryItemsAsync(value).SafeForget(logger); + } } } } @@ -95,6 +98,11 @@ internal class CultivationViewModel : ObservableObject, ISupportCancellation, IR /// public ObservableCollection? CultivateEntries { get => cultivateEntries; set => SetProperty(ref cultivateEntries, value); } + /// + /// 统计列表 + /// + public List? StatisticsItems { get => statisticsItems; set => SetProperty(ref statisticsItems, value); } + /// /// 打开界面命令 /// @@ -120,11 +128,10 @@ internal class CultivationViewModel : ObservableObject, ISupportCancellation, IR /// public ICommand SaveInventoryItemCommand { get; } - /// - public void Receive(CultivateProjectChangedMessage message) - { - UpdateCultivateEntriesAndInventoryItemsAsync(message.NewValue).SafeForget(logger); - } + /// + /// 展示统计物品命令 + /// + public ICommand UpdateStatisticsItemsCommand { get; } private async Task OpenUIAsync() { @@ -209,4 +216,18 @@ internal class CultivationViewModel : ObservableObject, ISupportCancellation, IR cultivationService.SaveInventoryItem(inventoryItem); } } + + private async Task UpdateStatisticsItemsAsync() + { + if (await metadataService.InitializeAsync().ConfigureAwait(true)) + { + if (SelectedProject != null) + { + List materials = await metadataService.GetMaterialsAsync().ConfigureAwait(false); + List temp = await cultivationService.GetStatisticsCultivateItemsAsync(SelectedProject, materials).ConfigureAwait(false); + await ThreadHelper.SwitchToMainThreadAsync(); + StatisticsItems = temp; + } + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs index b0c1268d..52e1bf56 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs @@ -51,6 +51,7 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation private LaunchScheme? selectedScheme; private ObservableCollection? gameAccounts; private GameAccount? selectedGameAccount; + private bool isExclusive; private bool isFullScreen; private bool isBorderless; private int screenWidth; @@ -106,15 +107,57 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation /// public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); } + /// + /// 是否为独占全屏 + /// + public bool IsExclusive + { + get => isExclusive; set + { + if (SetProperty(ref isExclusive, value)) + { + if (value) + { + IsFullScreen = true; + } + } + } + } + /// /// 全屏 /// - public bool IsFullScreen { get => isFullScreen; set => SetProperty(ref isFullScreen, value); } + public bool IsFullScreen + { + get => isFullScreen; set + { + if (SetProperty(ref isFullScreen, value)) + { + if (value) + { + IsBorderless = false; + } + } + } + } /// /// 无边框 /// - public bool IsBorderless { get => isBorderless; set => SetProperty(ref isBorderless, value); } + public bool IsBorderless + { + get => isBorderless; set + { + if (SetProperty(ref isBorderless, value)) + { + if (value) + { + IsExclusive = false; + IsFullScreen = false; + } + } + } + } /// /// 宽度 @@ -270,7 +313,7 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation SaveSetting(); - LaunchConfiguration configuration = new(IsFullScreen, IsBorderless, ScreenWidth, ScreenHeight, IsElevated && UnlockFps, TargetFps); + LaunchConfiguration configuration = new(IsExclusive, IsFullScreen, IsBorderless, ScreenWidth, ScreenHeight, IsElevated && UnlockFps, TargetFps); await gameService.LaunchAsync(configuration).ConfigureAwait(false); } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs index 924a053c..8a19d1a3 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs @@ -59,6 +59,11 @@ internal class UserViewModel : ObservableObject if (SetProperty(ref selectedUser, value)) { userService.Current = value; + + if (value != null) + { + value.SelectedUserGameRole = value.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen); + } } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/PatchClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/PatchClient.cs index 40f03622..ed729444 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/PatchClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/PatchClient.cs @@ -40,10 +40,10 @@ internal class PatchClient } /// - /// 更新信息 + /// 异步获取更新信息 /// - /// - /// + /// 取消令牌 + /// 更新信息 public Task GetPatchInformationAsync(CancellationToken token = default) { return httpClient.TryCatchGetFromJsonAsync(ApiEndpoints.PatcherHutaoStable, options, logger, token);