cultivation improvement

This commit is contained in:
DismissedLight
2022-12-29 15:03:21 +08:00
parent 26e6d2008e
commit 761049ec17
23 changed files with 535 additions and 255 deletions

View File

@@ -23,6 +23,11 @@ internal static class Activation
/// </summary>
public const string LaunchGame = "LaunchGame";
/// <summary>
/// 从剪贴板导入成就
/// </summary>
public const string ImportUIAFFromClipBoard = "ImportUIAFFromClipBoard";
private static readonly SemaphoreSlim ActivateSemaphore = new(1);
/// <summary>
@@ -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<INavigationService>()
.NavigateAsync<View.Page.AchievementPage>(navigationAwaiter, true)

View File

@@ -65,6 +65,11 @@ public class CultivateItem : ObservableObject
/// </summary>
public bool IsToday { get; }
/// <summary>
/// 对应背包物品的个数
/// </summary>
public uint InventoryItemCount { get; set; }
private void FlipIsFinished()
{
IsFinished = !IsFinished;

View File

@@ -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;
/// <summary>
/// 仅用于统计总数的养成物品
/// </summary>
public class StatisticsCultivateItem
{
/// <summary>
/// 构造一个新的统计用养成物品
/// </summary>
/// <param name="inner">材料</param>
/// <param name="entity">实体</param>
public StatisticsCultivateItem(Material inner, Entity.CultivateItem entity)
{
Inner = inner;
Count = entity.Count;
}
/// <summary>
/// 元数据
/// </summary>
public Material Inner { get; }
/// <summary>
/// 对应背包物品的个数
/// </summary>
public int Count { get; set; }
/// <summary>
/// 对应背包物品的个数
/// </summary>
public uint TotalCount { get; set; }
/// <summary>
/// 是否完成
/// </summary>
public bool IsFinished { get => Count >= TotalCount; }
/// <summary>
/// 格式化总数
/// </summary>
public string CountFormatted { get => $"{Count}/{TotalCount}"; }
}

View File

@@ -28,8 +28,14 @@ internal class Team : List<ComplexAvatar>
}
Rate = $"上场 {team.Rate} 次";
Name = TeamPopularNameParser.GetName(ids.ToHashSet());
}
/// <summary>
/// 队伍俗名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 上场次数
/// </summary>

View File

@@ -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;
/// <summary>
/// 队伍俗名解析器
/// </summary>
internal static class TeamPopularNameParser
{
/// <summary>
/// 已知的队伍名称
/// </summary>
private static readonly ImmutableDictionary<string, ImmutableHashSet<int>> KnownTeamNames = new Dictionary<string, ImmutableHashSet<int>>()
{
["雷国|雷行香班"] = new HashSet<int>() { AvatarIds.Shougun, AvatarIds.Xingqiu, AvatarIds.Xiangling, AvatarIds.Bennett, }.ToImmutableHashSet(),
["雷国|雷夜香班"] = new HashSet<int>() { AvatarIds.Shougun, AvatarIds.Yelan, AvatarIds.Xiangling, AvatarIds.Bennett, }.ToImmutableHashSet(),
["雷国|雷万香班"] = new HashSet<int>() { AvatarIds.Shougun, AvatarIds.Kazuha, AvatarIds.Xiangling, AvatarIds.Bennett, }.ToImmutableHashSet(),
["雷九|雷九万班"] = new HashSet<int>() { AvatarIds.Shougun, AvatarIds.Sara, AvatarIds.Kazuha, AvatarIds.Bennett, }.ToImmutableHashSet(),
["万达国际"] = new HashSet<int>() { AvatarIds.Kazuha, AvatarIds.Tartaglia, AvatarIds.Xiangling, AvatarIds.Bennett, }.ToImmutableHashSet(),
["胡行钟夜"] = new HashSet<int>() { AvatarIds.Hutao, AvatarIds.Xingqiu, AvatarIds.Zhongli, AvatarIds.Yelan, }.ToImmutableHashSet(),
["激晴|刻皇万妲"] = new HashSet<int>() { AvatarIds.Keqing, AvatarIds.Fischl, AvatarIds.Kazuha, AvatarIds.Nahida, }.ToImmutableHashSet(),
["永冻|神鹤万心"] = new HashSet<int>() { AvatarIds.Ayaka, AvatarIds.Shenhe, AvatarIds.Kazuha, AvatarIds.Kokomi, }.ToImmutableHashSet(),
["永冻|神钟万心"] = new HashSet<int>() { AvatarIds.Ayaka, AvatarIds.Zhongli, AvatarIds.Kazuha, AvatarIds.Kokomi, }.ToImmutableHashSet(),
["融甘|融化甘雨"] = new HashSet<int>() { AvatarIds.Ganyu, AvatarIds.Zhongli, AvatarIds.Xiangling, AvatarIds.Bennett, }.ToImmutableHashSet(),
["妮绽放|女主"] = new HashSet<int>() { AvatarIds.Nilou, AvatarIds.Kokomi, AvatarIds.Nahida, AvatarIds.PlayerGirl, }.ToImmutableHashSet(),
["妮绽放|男主"] = new HashSet<int>() { AvatarIds.Nilou, AvatarIds.Kokomi, AvatarIds.Nahida, AvatarIds.PlayerBoy, }.ToImmutableHashSet(),
["一斗岩队"] = new HashSet<int>() { AvatarIds.Itto, AvatarIds.Albedo, AvatarIds.Zhongli, AvatarIds.Gorou, }.ToImmutableHashSet(),
}.ToImmutableDictionary();
/// <summary>
/// 获取队伍名称
/// </summary>
/// <param name="ids">队伍集</param>
/// <returns>队伍名称</returns>
public static string GetName(HashSet<int> ids)
{
foreach (KeyValuePair<string, ImmutableHashSet<int>> entry in KnownTeamNames)
{
if (entry.Value.SetEquals(ids))
{
return entry.Key;
}
}
return "尚未命名";
}
}

View File

@@ -74,24 +74,24 @@ public enum WeaponType
/// <summary>
/// 法器
/// </summary>
[Description("单手剑")]
[Description("法器")]
WEAPON_CATALYST = 10,
/// <summary>
/// 双手剑
/// </summary>
[Description("手剑")]
[Description("手剑")]
WEAPON_CLAYMORE = 11,
/// <summary>
/// 弓
/// </summary>
[Description("单手剑")]
[Description("")]
WEAPON_BOW = 12,
/// <summary>
/// 长柄武器
/// </summary>
[Description("单手剑")]
[Description("长柄武器")]
WEAPON_POLE = 13,
}

View File

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

View File

@@ -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<AchievementService> logger;
private readonly DbCurrent<EntityArchive, Message.AchievementArchiveChangedMessage> 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);
}
/// <inheritdoc/>
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);
}
}
}

View File

@@ -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
/// <returns>任务</returns>
Task RemoveArchiveAsync(EntityArchive archive);
/// <summary>
/// 保存单个成就
/// </summary>
/// <param name="achievement">成就</param>
void SaveAchievement(BindingAchievement achievement);
/// <summary>
/// 保存成就
/// </summary>
@@ -69,6 +74,5 @@ internal interface IAchievementService
/// </summary>
/// <param name="newArchive">新存档</param>
/// <returns>存档添加结果</returns>
[ThreadAccess(ThreadAccessState.AnyThread)]
Task<ArchiveAddResult> TryAddArchiveAsync(EntityArchive newArchive);
}

View File

@@ -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
}
}
/// <inheritdoc/>
public async Task<List<Model.Binding.Cultivation.StatisticsCultivateItem>> GetStatisticsCultivateItemsAsync(CultivateProject cultivateProject, List<Model.Metadata.Material> materials)
{
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
Guid projectId = cultivateProject.InnerId;
List<Model.Binding.Cultivation.StatisticsCultivateItem> resultItems = new();
List<CultivateEntry> entries = await appDbContext.CultivateEntries
.AsNoTracking()
.Where(e => e.ProjectId == projectId)
.ToListAsync()
.ConfigureAwait(false);
foreach (CultivateEntry entry in entries)
{
Guid entryId = entry.InnerId;
List<CultivateItem> 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<InventoryItem> 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();
}
}
/// <inheritdoc/>
public async Task RemoveCultivateEntryAsync(Guid entryId)
{

View File

@@ -44,6 +44,14 @@ internal interface ICultivationService
/// <returns>项目集合</returns>
ObservableCollection<CultivateProject> GetProjectCollection();
/// <summary>
/// 异步获取统计物品列表
/// </summary>
/// <param name="cultivateProject">养成计划</param>
/// <param name="materials">元数据</param>
/// <returns>统计物品列表</returns>
Task<List<StatisticsCultivateItem>> GetStatisticsCultivateItemsAsync(CultivateProject cultivateProject, List<Material> materials);
/// <summary>
/// 删除养成清单
/// </summary>

View File

@@ -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();

View File

@@ -8,6 +8,11 @@ namespace Snap.Hutao.Service.Game;
/// </summary>
internal readonly struct LaunchConfiguration
{
/// <summary>
/// 是否为独占全屏
/// </summary>
public readonly bool IsExclusive;
/// <summary>
/// 是否全屏,全屏时无边框设置将被覆盖
/// </summary>
@@ -41,14 +46,16 @@ internal readonly struct LaunchConfiguration
/// <summary>
/// 构造一个新的启动配置
/// </summary>
/// <param name="isExclusive">独占全屏</param>
/// <param name="isFullScreen">全屏</param>
/// <param name="isBorderless">无边框</param>
/// <param name="screenWidth">宽度</param>
/// <param name="screenHeight">高度</param>
/// <param name="unlockFps">解锁帧率</param>
/// <param name="targetFps">目标帧率</param>
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;
}
/// <summary>
/// 窗口模式字符串
/// </summary>
public string? WindowMode
{
get
{
return IsFullScreen ? "exclusive" : IsBorderless ? "borderless" : null;
}
}
}

View File

@@ -139,7 +139,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0-preview2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0-preview3" />
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Behaviors" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />

View File

@@ -15,6 +15,10 @@
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<Page.Resources>
<shc:BindingProxy x:Key="BindingProxy" DataContext="{Binding}"/>
</Page.Resources>
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
@@ -77,12 +81,8 @@
Command="{Binding RemoveArchiveCommand}"
Icon="{shcm:FontIcon Glyph=&#xE74D;}"
Label="删除当前存档"/>
<AppBarSeparator/>
<AppBarButton
Command="{Binding RefreshFinishPercentCommand}"
Icon="{shcm:FontIcon Glyph=&#xE72C;}"
Label="刷新成就进度"/>
<AppBarButton Icon="{shcm:FontIcon Glyph=&#xE8B5;}" Label="导入">
<AppBarButton.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedRight">
@@ -102,6 +102,7 @@
Icon="{shcm:FontIcon Glyph=&#xEDE1;}"
Label="导出"/>
<AppBarSeparator/>
<AppBarToggleButton
Command="{Binding SortIncompletedSwitchCommand}"
Icon="{shcm:FontIcon Glyph=&#xE8CB;}"
@@ -182,6 +183,8 @@
Grid.Column="1"
Margin="6,0,12,0"
Padding="16,0,0,0"
Command="{Binding Path=DataContext.SaveAchievementCommand, Source={StaticResource BindingProxy}}"
CommandParameter="{Binding}"
IsChecked="{Binding IsChecked, Mode=TwoWay}"
Style="{StaticResource DefaultCheckBoxStyle}">
<CheckBox.Content>

View File

@@ -49,6 +49,54 @@
<Pivot>
<Pivot.RightHeader>
<CommandBar DefaultLabelPosition="Right">
<AppBarButton
Command="{Binding UpdateStatisticsItemsCommand}"
Icon="{shcm:FontIcon Glyph=&#xEB05;}"
Label="材料统计">
<AppBarButton.Flyout>
<Flyout Placement="Bottom">
<Flyout.FlyoutPresenterStyle>
<Style BasedOn="{StaticResource DefaultFlyoutPresenterStyle}" TargetType="FlyoutPresenter">
<Setter Property="MaxHeight" Value="480"/>
<Setter Property="Padding" Value="0,2,0,2"/>
<Setter Property="Background" Value="{ThemeResource FlyoutPresenterBackground}"/>
</Style>
</Flyout.FlyoutPresenterStyle>
<ItemsControl Margin="16,0,16,16" ItemsSource="{Binding StatisticsItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="0,16,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="160"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<shvc:ItemIcon
Grid.Column="0"
Width="32"
Height="32"
Icon="{Binding Inner.Icon, Converter={StaticResource ItemIconConverter}}"
Quality="{Binding Inner.RankLevel}"/>
<TextBlock
Grid.Column="1"
Margin="16,0,0,0"
VerticalAlignment="Center"
Text="{Binding Inner.Name}"
TextTrimming="CharacterEllipsis"/>
<TextBlock
Grid.Column="2"
Margin="16,0,4,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{Binding CountFormatted}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Flyout>
</AppBarButton.Flyout>
</AppBarButton>
<AppBarSeparator/>
<AppBarElementContainer>
<ComboBox
Height="36"

View File

@@ -10,6 +10,7 @@
xmlns:shc="using:Snap.Hutao.Control"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shmbh="using:Snap.Hutao.Model.Binding.Hutao"
xmlns:shv="using:Snap.Hutao.ViewModel"
xmlns:shvc="using:Snap.Hutao.View.Control"
d:DataContext="{d:DesignInstance shv:HutaoDatabaseViewModel}"
@@ -20,6 +21,47 @@
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources>
<DataTemplate x:Key="TeamItemTemplate" x:DataType="shmbh:Team">
<Border Margin="12,0,12,12" Style="{StaticResource BorderCardStyle}">
<Grid Margin="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<ItemsControl HorizontalAlignment="Left" ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwuc:UniformGrid ColumnSpacing="6" Columns="4"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<shvc:ItemIcon
Width="48"
Height="48"
Icon="{Binding Icon}"
Quality="{Binding Quality}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Text="{Binding Rate}"/>
<TextBlock
Margin="0,2,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Name}"/>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</Page.Resources>
<Grid>
<Pivot>
<Pivot.RightHeader>
@@ -97,6 +139,39 @@
</Pivot.ItemTemplate>
</Pivot>
</PivotItem>
<PivotItem Header="队伍出场">
<Pivot ItemsSource="{Binding TeamAppearances}">
<Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Floor}"/>
</DataTemplate>
</Pivot.HeaderTemplate>
<Pivot.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Column="0" Margin="0,12,0,0">
<ItemsControl
ItemTemplate="{StaticResource TeamItemTemplate}"
ItemsPanel="{StaticResource ItemsStackPanelTemplate}"
ItemsSource="{Binding Up}"/>
</ScrollViewer>
<ScrollViewer Grid.Column="1" Margin="0,12,0,0">
<ItemsControl
ItemTemplate="{StaticResource TeamItemTemplate}"
ItemsPanel="{StaticResource ItemsStackPanelTemplate}"
ItemsSource="{Binding Down}"/>
</ScrollViewer>
</Grid>
</DataTemplate>
</Pivot.ItemTemplate>
</Pivot>
</PivotItem>
<PivotItem Header="角色持有">
<Grid>
<Grid.RowDefinitions>
@@ -231,117 +306,6 @@
</ScrollViewer>
</Grid>
</PivotItem>
<PivotItem Header="队伍出场">
<Pivot ItemsSource="{Binding TeamAppearances}">
<Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Floor}"/>
</DataTemplate>
</Pivot.HeaderTemplate>
<Pivot.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Column="0" Margin="0,12,0,0">
<ItemsControl ItemsSource="{Binding Up}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
Margin="12,0,12,12"
Background="{StaticResource CardBackgroundFillColorDefault}"
CornerRadius="{StaticResource CompatCornerRadius}">
<Grid Margin="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<ItemsControl HorizontalAlignment="Left" ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwuc:UniformGrid ColumnSpacing="6" Columns="4"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<shvc:ItemIcon
Width="48"
Height="48"
Icon="{Binding Icon}"
Quality="{Binding Quality}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Rate}"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<ScrollViewer Grid.Column="1" Margin="0,12,0,0">
<ItemsControl ItemsSource="{Binding Down}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
Margin="12,0,12,12"
Background="{StaticResource CardBackgroundFillColorDefault}"
CornerRadius="{StaticResource CompatCornerRadius}">
<Grid Margin="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<ItemsControl HorizontalAlignment="Left" ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwuc:UniformGrid ColumnSpacing="6" Columns="4"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<shvc:ItemIcon
Width="48"
Height="48"
Icon="{Binding Icon}"
Quality="{Binding Quality}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Rate}"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</DataTemplate>
</Pivot.ItemTemplate>
</Pivot>
</PivotItem>
</Pivot>
</Grid>
</shc:ScopedPage>

View File

@@ -158,6 +158,17 @@
</sc:SettingExpander>
</sc:SettingsGroup>
<sc:SettingsGroup Header="外观">
<sc:Setting
Description="与游戏内浏览器不兼容,切屏等操作也能使游戏闪退"
Header="独占全屏"
Icon="&#xE740;">
<sc:Setting.ActionContent>
<ToggleSwitch
Width="120"
IsOn="{Binding IsExclusive, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchSettingStyle}"/>
</sc:Setting.ActionContent>
</sc:Setting>
<sc:Setting
Description="覆盖默认的全屏状态"
Header="全屏"

View File

@@ -10,6 +10,7 @@ using Snap.Hutao.Control;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.DataTransfer;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Threading.CodeAnalysis;
using Snap.Hutao.Extension;
using Snap.Hutao.Factory.Abstraction;
@@ -33,26 +34,22 @@ namespace Snap.Hutao.ViewModel;
/// 成就视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
[SuppressMessage("", "SA1124")]
internal class AchievementViewModel
: ObservableObject,
ISupportCancellation,
INavigationRecipient,
IDisposable,
IRecipient<AchievementArchiveChangedMessage>
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<bool> openUICompletionSource = new();
private bool disposed;
private AdvancedCollectionView? achievements;
private List<Model.Binding.Achievement.AchievementGoal>? achievementGoals;
private Model.Binding.Achievement.AchievementGoal? selectedAchievementGoal;
@@ -94,9 +91,7 @@ internal class AchievementViewModel
RemoveArchiveCommand = asyncRelayCommandFactory.Create(RemoveArchiveAsync);
SearchAchievementCommand = new RelayCommand<string>(SearchAchievement);
SortIncompletedSwitchCommand = new RelayCommand(UpdateAchievementsSort);
RefreshFinishPercentCommand = new RelayCommand(UpdateAchievementFinishPercent);
messenger.Register(this);
SaveAchievementCommand = new RelayCommand<Model.Binding.Achievement.Achievement>(SaveAchievement);
}
/// <inheritdoc/>
@@ -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; }
/// <summary>
/// 刷新完成百分比命令
/// 保存单个成就命令
/// </summary>
public ICommand RefreshFinishPercentCommand { get; }
/// <inheritdoc/>
public void Receive(AchievementArchiveChangedMessage message)
{
HandleArchiveChangeAsync(message.OldValue, message.NewValue).SafeForget();
}
/// <inheritdoc/>
public void Dispose()
{
if (!disposed)
{
if (Achievements != null && SelectedArchive != null)
{
achievementService.SaveAchievements(SelectedArchive, (Achievements.Source as IList<Model.Binding.Achievement.Achievement>)!);
}
disposed = true;
}
}
public ICommand SaveAchievementCommand { get; }
/// <inheritdoc/>
public async Task<bool> 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<ContentDialogResult> ShowImportResultDialogAsync(string title, string message)
{
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
ContentDialog dialog = new()
{
Title = title,
Content = message,
PrimaryButtonText = "确认",
DefaultButton = ContentDialogButton.Primary,
XamlRoot = mainWindow.Content.XamlRoot,
};
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
return dialog.InitializeWithWindow(mainWindow).ShowAsync().AsTask();
return dialog.ShowAsync().AsTask();
}
private static Task<ContentDialogResult> ShowImportFailDialogAsync(string message)
{
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
ContentDialog dialog = new()
{
Title = "导入失败",
Content = message,
PrimaryButtonText = "确认",
DefaultButton = ContentDialogButton.Primary,
XamlRoot = mainWindow.Content.XamlRoot,
};
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
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<Model.Binding.Achievement.Achievement>)!);
}
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<Model.Metadata.Achievement.Achievement> rawAchievements = await metadataService.GetAchievementsAsync(CancellationToken).ConfigureAwait(false);
List<Model.Binding.Achievement.Achievement> 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<MainWindow>();
@@ -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<Model.Binding.Achievement.Achievement>)!);
await ThreadHelper.SwitchToMainThreadAsync();
IPickerFactory pickerFactory = Ioc.Default.GetRequiredService<IPickerFactory>();
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<Model.Metadata.Achievement.Achievement> rawAchievements = await metadataService.GetAchievementsAsync(CancellationToken).ConfigureAwait(false);
List<Model.Binding.Achievement.Achievement> 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<int, AchievementGoalAggregation> counter = AchievementGoals.ToDictionary(x => x.Id, x => new AchievementGoalAggregation(x));
foreach (Model.Binding.Achievement.Achievement achievement in Achievements.OfType<Model.Binding.Achievement.Achievement>())
Dictionary<int, GoalAggregation> counter = AchievementGoals.ToDictionary(x => x.Id, x => new GoalAggregation(x));
foreach (Model.Binding.Achievement.Achievement achievement in Achievements.SourceCollection.OfType<Model.Binding.Achievement.Achievement>())
{
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;
}

View File

@@ -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;
/// 养成视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal class CultivationViewModel : ObservableObject, ISupportCancellation, IRecipient<CultivateProjectChangedMessage>
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<Model.Binding.Inventory.InventoryItem>? inventoryItems;
private ObservableCollection<Model.Binding.Cultivation.CultivateEntry>? cultivateEntries;
private List<StatisticsCultivateItem>? statisticsItems;
/// <summary>
/// 构造一个新的养成视图模型
@@ -40,14 +42,12 @@ internal class CultivationViewModel : ObservableObject, ISupportCancellation, IR
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
/// <param name="metadataService">元数据服务</param>
/// <param name="logger">日志器</param>
/// <param name="messenger">消息器</param>
public CultivationViewModel(
ICultivationService cultivationService,
IInfoBarService infoBarService,
IAsyncRelayCommandFactory asyncRelayCommandFactory,
IMetadataService metadataService,
ILogger<CultivationViewModel> logger,
IMessenger messenger)
ILogger<CultivationViewModel> logger)
{
this.cultivationService = cultivationService;
this.infoBarService = infoBarService;
@@ -59,8 +59,7 @@ internal class CultivationViewModel : ObservableObject, ISupportCancellation, IR
RemoveProjectCommand = asyncRelayCommandFactory.Create<CultivateProject>(RemoveProjectAsync);
RemoveEntryCommand = asyncRelayCommandFactory.Create<Model.Binding.Cultivation.CultivateEntry>(RemoveEntryAsync);
SaveInventoryItemCommand = new RelayCommand<Model.Binding.Inventory.InventoryItem>(SaveInventoryItem);
messenger.Register(this);
UpdateStatisticsItemsCommand = asyncRelayCommandFactory.Create(UpdateStatisticsItemsAsync);
}
/// <inheritdoc/>
@@ -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
/// </summary>
public ObservableCollection<Model.Binding.Cultivation.CultivateEntry>? CultivateEntries { get => cultivateEntries; set => SetProperty(ref cultivateEntries, value); }
/// <summary>
/// 统计列表
/// </summary>
public List<StatisticsCultivateItem>? StatisticsItems { get => statisticsItems; set => SetProperty(ref statisticsItems, value); }
/// <summary>
/// 打开界面命令
/// </summary>
@@ -120,11 +128,10 @@ internal class CultivationViewModel : ObservableObject, ISupportCancellation, IR
/// </summary>
public ICommand SaveInventoryItemCommand { get; }
/// <inheritdoc/>
public void Receive(CultivateProjectChangedMessage message)
{
UpdateCultivateEntriesAndInventoryItemsAsync(message.NewValue).SafeForget(logger);
}
/// <summary>
/// 展示统计物品命令
/// </summary>
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<Model.Metadata.Material> materials = await metadataService.GetMaterialsAsync().ConfigureAwait(false);
List<StatisticsCultivateItem> temp = await cultivationService.GetStatisticsCultivateItemsAsync(SelectedProject, materials).ConfigureAwait(false);
await ThreadHelper.SwitchToMainThreadAsync();
StatisticsItems = temp;
}
}
}
}

View File

@@ -51,6 +51,7 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
private LaunchScheme? selectedScheme;
private ObservableCollection<GameAccount>? 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
/// </summary>
public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); }
/// <summary>
/// 是否为独占全屏
/// </summary>
public bool IsExclusive
{
get => isExclusive; set
{
if (SetProperty(ref isExclusive, value))
{
if (value)
{
IsFullScreen = true;
}
}
}
}
/// <summary>
/// 全屏
/// </summary>
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;
}
}
}
}
/// <summary>
/// 无边框
/// </summary>
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;
}
}
}
}
/// <summary>
/// 宽度
@@ -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);
}

View File

@@ -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);
}
}
}
}

View File

@@ -40,10 +40,10 @@ internal class PatchClient
}
/// <summary>
/// 更新信息
/// 异步获取更新信息
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
/// <param name="token">取消令牌</param>
/// <returns>更新信息</returns>
public Task<Patch.PatchInformation?> GetPatchInformationAsync(CancellationToken token = default)
{
return httpClient.TryCatchGetFromJsonAsync<Patch.PatchInformation>(ApiEndpoints.PatcherHutaoStable, options, logger, token);