make achievement great again

This commit is contained in:
DismissedLight
2024-06-29 20:35:45 +08:00
parent ff785387dc
commit 98b5436828
61 changed files with 411 additions and 419 deletions

View File

@@ -322,6 +322,7 @@ dotnet_diagnostic.CA2227.severity = suggestion
dotnet_diagnostic.CA2251.severity = suggestion
csharp_style_prefer_primary_constructors = false:none
dotnet_diagnostic.SA1124.severity = none
[*.vb]
#### 命名样式 ####

View File

@@ -1,16 +1,104 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database.Abstraction;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.UI.Xaml.Data;
namespace Snap.Hutao.Core.Database;
// The scope of the view follows the scope of the service provider.
internal sealed class AdvancedDbCollectionView<TEntity> : AdvancedCollectionView<TEntity>
where TEntity : class, IAdvancedCollectionViewItem, ISelectable
{
public AdvancedDbCollectionView(IList<TEntity> source)
private readonly IServiceProvider serviceProvider;
private bool detached;
public AdvancedDbCollectionView(IList<TEntity> source, IServiceProvider serviceProvider)
: base(source)
{
this.serviceProvider = serviceProvider;
}
public void Detach()
{
detached = true;
}
protected override void OnCurrentChangedOverride()
{
if (serviceProvider is null || detached)
{
return;
}
TEntity? currentItem = CurrentItem;
foreach (TEntity item in Source)
{
item.IsSelected = ReferenceEquals(item, currentItem);
}
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
dbContext.Set<TEntity>().ExecuteUpdate(update => update.SetProperty(entity => entity.IsSelected, false));
if (currentItem is not null)
{
dbContext.Set<TEntity>().UpdateAndSave(currentItem);
}
}
}
}
// The scope of the view follows the scope of the service provider.
[SuppressMessage("", "SA1402")]
internal sealed class AdvancedDbCollectionView<TEntityAccess, TEntity> : AdvancedCollectionView<TEntityAccess>
where TEntityAccess : class, IEntityAccess<TEntity>, IAdvancedCollectionViewItem
where TEntity : class, ISelectable
{
private readonly IServiceProvider serviceProvider;
private bool detached;
public AdvancedDbCollectionView(IList<TEntityAccess> source, IServiceProvider serviceProvider)
: base(source)
{
this.serviceProvider = serviceProvider;
}
public void Detach()
{
detached = true;
}
protected override void OnCurrentChangedOverride()
{
if (serviceProvider is null || detached)
{
return;
}
TEntityAccess? currentItem = CurrentItem;
foreach (TEntityAccess item in Source)
{
item.Entity.IsSelected = ReferenceEquals(item, currentItem);
}
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
dbContext.Set<TEntity>().ExecuteUpdate(update => update.SetProperty(entity => entity.IsSelected, false));
if (currentItem is not null)
{
dbContext.Set<TEntity>().UpdateAndSave(currentItem.Entity);
}
}
}
}

View File

@@ -64,13 +64,13 @@ internal sealed class ObservableReorderableDbCollection<TEntity> : ObservableCol
}
[SuppressMessage("", "SA1402")]
internal sealed class ObservableReorderableDbCollection<TEntityOnly, TEntity> : ObservableCollection<TEntityOnly>
where TEntityOnly : class, IEntityAccess<TEntity>
internal sealed class ObservableReorderableDbCollection<TEntityAccess, TEntity> : ObservableCollection<TEntityAccess>
where TEntityAccess : class, IEntityAccess<TEntity>
where TEntity : class, IReorderable
{
private readonly IServiceProvider serviceProvider;
public ObservableReorderableDbCollection(List<TEntityOnly> items, IServiceProvider serviceProvider)
public ObservableReorderableDbCollection(List<TEntityAccess> items, IServiceProvider serviceProvider)
: base(AdjustIndex(items.SortBy(x => x.Entity.Index)))
{
this.serviceProvider = serviceProvider;
@@ -89,12 +89,12 @@ internal sealed class ObservableReorderableDbCollection<TEntityOnly, TEntity> :
}
}
private static List<TEntityOnly> AdjustIndex(List<TEntityOnly> list)
private static List<TEntityAccess> AdjustIndex(List<TEntityAccess> list)
{
Span<TEntityOnly> span = CollectionsMarshal.AsSpan(list);
Span<TEntityAccess> span = CollectionsMarshal.AsSpan(list);
for (int i = 0; i < list.Count; i++)
{
ref readonly TEntityOnly item = ref span[i];
ref readonly TEntityAccess item = ref span[i];
item.Entity.Index = i;
}
@@ -103,14 +103,14 @@ internal sealed class ObservableReorderableDbCollection<TEntityOnly, TEntity> :
private void OnReorder()
{
AdjustIndex((List<TEntityOnly>)Items);
AdjustIndex((List<TEntityAccess>)Items);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
foreach (ref readonly TEntityOnly item in CollectionsMarshal.AsSpan((List<TEntityOnly>)Items))
foreach (ref readonly TEntityAccess item in CollectionsMarshal.AsSpan((List<TEntityAccess>)Items))
{
dbSet.UpdateAndSave(item.Entity);
}

View File

@@ -9,6 +9,7 @@ using Snap.Hutao.Model.Entity.Database;
namespace Snap.Hutao.Core.Database;
[Obsolete]
[ConstructorGenerated]
internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
where TEntity : class, ISelectable
@@ -63,6 +64,7 @@ internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
}
}
[Obsolete]
[ConstructorGenerated]
internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
where TEntityOnly : class, IEntityAccess<TEntity>

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
/// <summary>
/// 指示此特性附加的属性会在属性改变后会异步地设置的其他属性
/// </summary>
[Obsolete]
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
internal sealed class AlsoAsyncSetsAttribute : Attribute
{
public AlsoAsyncSetsAttribute(string propertyName)
{
}
public AlsoAsyncSetsAttribute(string propertyName1, string propertyName2)
{
}
public AlsoAsyncSetsAttribute(params string[] propertyNames)
{
}
}

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
/// <summary>
/// 指示此特性附加的属性会在属性改变后会设置的其他属性
/// </summary>
[Obsolete]
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
internal sealed class AlsoSetsAttribute : Attribute
{
public AlsoSetsAttribute(string propertyName)
{
}
public AlsoSetsAttribute(string propertyName1, string propertyName2)
{
}
public AlsoSetsAttribute(params string[] propertyNames)
{
}
}

View File

@@ -58,5 +58,7 @@ internal sealed class TempFileStream : Stream
stream.Dispose();
File.Delete(path);
}
base.Dispose(disposing);
}
}

View File

@@ -24,6 +24,7 @@ internal static class TaskExtension
return new(task);
}
[Obsolete("SafeForget without logger is not recommended.")]
public static async void SafeForget(this Task task)
{
try
@@ -100,6 +101,7 @@ internal static class TaskExtension
}
}
[Obsolete("SafeForget without logger is not recommended.")]
public static async void SafeForget(this ValueTask task)
{
try

View File

@@ -78,23 +78,6 @@ internal static class ListExtension
return true;
}
public static bool RemoveFirstWhere<T>(this List<T> list, Func<T, bool> shouldRemovePredicate)
{
Span<T> span = CollectionsMarshal.AsSpan(list);
ref T reference = ref MemoryMarshal.GetReference(span);
for (int i = 0; i < span.Length; i++)
{
if (shouldRemovePredicate.Invoke(Unsafe.Add(ref reference, i)))
{
list.RemoveAt(i);
return true;
}
}
return false;
}
public static void RemoveLast<T>(this IList<T> collection)
{
collection.RemoveAt(collection.Count - 1);

View File

@@ -1,15 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
namespace Snap.Hutao.Message;
/// <summary>
/// 成就存档切换消息
/// </summary>
[HighQuality]
[Obsolete]
internal sealed class AchievementArchiveChangedMessage : ValueChangedMessage<AchievementArchive>
{
}

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Database.Abstraction;
using Snap.Hutao.UI.Xaml.Data;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -12,7 +13,9 @@ namespace Snap.Hutao.Model.Entity;
/// 成就存档
/// </summary>
[Table("achievement_archives")]
internal sealed class AchievementArchive : ISelectable, IMappingFrom<AchievementArchive, string>
internal sealed class AchievementArchive : ISelectable,
IAdvancedCollectionViewItem,
IMappingFrom<AchievementArchive, string>
{
/// <summary>
/// 内部Id
@@ -40,4 +43,12 @@ internal sealed class AchievementArchive : ISelectable, IMappingFrom<Achievement
{
return new() { Name = name };
}
public object? GetPropertyValue(string name)
{
return name switch
{
_ => default!,
};
}
}

View File

@@ -66,6 +66,7 @@ internal static class AppDbServiceExtension
return service.Execute(dbset => dbset.AddAndSave(entity));
}
[Obsolete]
public static ValueTask<int> AddAsync<TEntity>(this IAppDbService<TEntity> service, TEntity entity, CancellationToken token = default)
where TEntity : class
{
@@ -78,6 +79,7 @@ internal static class AppDbServiceExtension
return service.Execute(dbset => dbset.AddRangeAndSave(entities));
}
[Obsolete]
public static ValueTask<int> AddRangeAsync<TEntity>(this IAppDbService<TEntity> service, IEnumerable<TEntity> entities, CancellationToken token = default)
where TEntity : class
{
@@ -144,6 +146,7 @@ internal static class AppDbServiceExtension
return service.Execute(dbset => dbset.UpdateAndSave(entity));
}
[Obsolete]
public static ValueTask<int> UpdateAsync<TEntity>(this IAppDbService<TEntity> service, TEntity entity, CancellationToken token = default)
where TEntity : class
{
@@ -162,6 +165,7 @@ internal static class AppDbServiceExtension
return service.Execute(dbset => dbset.Where(predicate).ExecuteDelete());
}
[Obsolete]
public static ValueTask<int> DeleteAsync<TEntity>(this IAppDbService<TEntity> service, TEntity entity, CancellationToken token = default)
where TEntity : class
{

View File

@@ -8,7 +8,6 @@ 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;
@@ -18,32 +17,17 @@ namespace Snap.Hutao.Service.Achievement;
[Injection(InjectAs.Scoped, typeof(IAchievementService))]
internal sealed partial class AchievementService : IAchievementService
{
private readonly ScopedDbCurrent<AchievementArchive, Message.AchievementArchiveChangedMessage> dbCurrent;
private readonly AchievementDbBulkOperation achievementDbBulkOperation;
private readonly IAchievementDbService achievementDbService;
private readonly IServiceProvider serviceProvider;
private readonly RuntimeOptions runtimeOptions;
private readonly ITaskContext taskContext;
private ObservableCollection<AchievementArchive>? archiveCollection;
private AdvancedDbCollectionView<AchievementArchive>? archivesView;
public AchievementArchive? CurrentArchive
public AdvancedDbCollectionView<AchievementArchive> Archives
{
get => dbCurrent.Current;
set => dbCurrent.Current = value;
}
public ObservableCollection<AchievementArchive> ArchiveCollection
{
get
{
if (archiveCollection is null)
{
archiveCollection = achievementDbService.GetAchievementArchiveCollection();
CurrentArchive = archiveCollection.SelectedOrDefault();
}
return archiveCollection;
}
get => archivesView ??= new(achievementDbService.GetAchievementArchiveCollection(), serviceProvider);
}
public List<AchievementView> GetAchievementViewList(AchievementArchive archive, AchievementServiceMetadataContext context)
@@ -69,31 +53,27 @@ internal sealed partial class AchievementService : IAchievementService
return ArchiveAddResultKind.InvalidName;
}
ArgumentNullException.ThrowIfNull(archiveCollection);
ArgumentNullException.ThrowIfNull(archivesView);
if (archiveCollection.Any(a => a.Name == newArchive.Name))
if (archivesView.SourceCollection.Any(a => a.Name == newArchive.Name))
{
return ArchiveAddResultKind.AlreadyExists;
}
// Sync cache
await taskContext.SwitchToMainThreadAsync();
archiveCollection.Add(newArchive);
// Sync database
await taskContext.SwitchToBackgroundAsync();
CurrentArchive = newArchive;
archivesView.Add(newArchive);
archivesView.MoveCurrentTo(newArchive);
return ArchiveAddResultKind.Added;
}
public async ValueTask RemoveArchiveAsync(AchievementArchive archive)
{
ArgumentNullException.ThrowIfNull(archiveCollection);
ArgumentNullException.ThrowIfNull(archivesView);
// Sync cache
await taskContext.SwitchToMainThreadAsync();
archiveCollection.Remove(archive);
archivesView.Remove(archive);
// Sync database
await taskContext.SwitchToBackgroundAsync();

View File

@@ -1,64 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.InterChange.Achievement;
using Snap.Hutao.ViewModel.Achievement;
using System.Collections.ObjectModel;
using EntityArchive = Snap.Hutao.Model.Entity.AchievementArchive;
namespace Snap.Hutao.Service.Achievement;
/// <summary>
/// 成就服务抽象
/// </summary>
[HighQuality]
internal interface IAchievementService
{
/// <summary>
/// 当前存档
/// </summary>
EntityArchive? CurrentArchive { get; set; }
AdvancedDbCollectionView<EntityArchive> Archives { get; }
/// <summary>
/// 获取用于绑定的成就存档集合
/// </summary>
ObservableCollection<EntityArchive> ArchiveCollection { get; }
/// <summary>
/// 异步导出到UIAF
/// </summary>
/// <param name="selectedArchive">存档</param>
/// <returns>UIAF</returns>
ValueTask<UIAF> ExportToUIAFAsync(EntityArchive selectedArchive);
List<AchievementView> GetAchievementViewList(EntityArchive archive, AchievementServiceMetadataContext context);
/// <summary>
/// 异步导入UIAF数据
/// </summary>
/// <param name="archive">用户</param>
/// <param name="list">UIAF数据</param>
/// <param name="strategy">选项</param>
/// <returns>导入结果</returns>
ValueTask<ImportResult> ImportFromUIAFAsync(EntityArchive archive, List<UIAFItem> list, ImportStrategyKind strategy);
/// <summary>
/// 异步移除存档
/// </summary>
/// <param name="archive">待移除的存档</param>
/// <returns>任务</returns>
ValueTask RemoveArchiveAsync(EntityArchive archive);
/// <summary>
/// 保存单个成就
/// </summary>
/// <param name="achievement">成就</param>
void SaveAchievement(AchievementView achievement);
/// <summary>
/// 尝试添加存档
/// </summary>
/// <param name="newArchive">新存档</param>
/// <returns>存档添加结果</returns>
ValueTask<ArchiveAddResultKind> AddArchiveAsync(EntityArchive newArchive);
}

View File

@@ -314,7 +314,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Snap.Hutao.SourceGeneration" Version="1.1.0">
<PackageReference Include="Snap.Hutao.SourceGeneration" Version="1.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -53,7 +53,7 @@ internal class ScopedPage : Page
TViewModel viewModel = pageScope.ServiceProvider.GetRequiredService<TViewModel>();
using (viewModel.DisposeLock.Enter())
{
viewModel.IsViewDisposed = false;
viewModel.Resurrect();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
viewModel.DeferContentLoader = new DeferContentLoader(this);
}
@@ -106,10 +106,10 @@ internal class ScopedPage : Page
viewCancellationTokenSource.Cancel();
IViewModel viewModel = (IViewModel)DataContext;
// Wait to ensure viewmodel operation is completed
using (viewModel.DisposeLock.Enter())
{
// Wait to ensure viewmodel operation is completed
viewModel.IsViewDisposed = true;
viewModel.Uninitialize();
// Dispose the scope
pageScope.Dispose();

View File

@@ -689,9 +689,9 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
return;
}
OnCurrentChangedOverride();
CurrentChanged?.Invoke(this, default!);
OnPropertyChanged(nameof(CurrentItem));
OnCurrentChangedOverride();
}
private void OnVectorChanged(IVectorChangedEventArgs e)

View File

@@ -21,7 +21,7 @@
Style="{ThemeResource DefaultButtonStyle}"
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Button.Resources>

View File

@@ -24,7 +24,7 @@
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Button.Resources>

View File

@@ -23,7 +23,7 @@
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Button.Resources>

View File

@@ -23,7 +23,7 @@
Style="{ThemeResource DefaultButtonStyle}"
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Grid CornerRadius="{ThemeResource ControlCornerRadius}">
<Grid Margin="12" ColumnSpacing="16">

View File

@@ -14,7 +14,7 @@
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<UserControl.Resources>

View File

@@ -182,17 +182,17 @@
x:Key="AchievementListViewItemStyle"
BasedOn="{StaticResource DefaultListViewItemStyle}"
TargetType="ListViewItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="0,4,8,0"/>
<Setter Property="Padding" Value="0"/>
</Style>
</Page.Resources>
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Grid Visibility="{Binding IsInitialized, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid Visibility="{Binding SelectedArchive, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
<Grid Visibility="{Binding Archives.CurrentItem, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
@@ -261,7 +261,7 @@
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding Archives, Mode=OneWay}"
SelectedItem="{Binding SelectedArchive, Mode=TwoWay}"
SelectedItem="{Binding Archives.CurrentItem, Mode=TwoWay}"
Style="{ThemeResource CommandBarComboBoxStyle}"/>
</shuxc:SizeRestrictedContentControl>
</AppBarElementContainer>
@@ -294,7 +294,6 @@
Icon="{shuxm:FontIcon Glyph=&#xEDE1;}"
Label="{shuxm:ResourceString Name=ViewPageAchievementExportLabel}"/>
<AppBarSeparator/>
<AppBarToggleButton
Command="{Binding SortUncompletedSwitchCommand}"
Icon="{shuxm:FontIcon Glyph=&#xE8CB;}"
@@ -322,7 +321,7 @@
ItemContainerStyle="{StaticResource AchievementGoalListViewItemStyle}"
ItemTemplate="{StaticResource AchievementGoalListTemplate}"
ItemsSource="{Binding AchievementGoals}"
SelectedItem="{Binding SelectedAchievementGoal, Mode=TwoWay}"
SelectedItem="{Binding AchievementGoals.CurrentItem, Mode=TwoWay}"
SelectionMode="Single">
<mxi:Interaction.Behaviors>
<shuxb:SelectedItemInViewBehavior/>
@@ -347,7 +346,7 @@
HorizontalContentAlignment="Stretch"
ItemTemplate="{StaticResource AchievementGoalGridTemplate}"
ItemsSource="{Binding AchievementGoals}"
SelectedItem="{Binding SelectedAchievementGoal, Mode=TwoWay}"
SelectedItem="{Binding AchievementGoals.CurrentItem, Mode=TwoWay}"
SelectionMode="Single">
<GridView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultGridViewItemStyle}" TargetType="GridViewItem">
@@ -372,7 +371,7 @@
</Border>
</Grid>
<Grid Visibility="{Binding SelectedArchive, Converter={StaticResource EmptyObjectToVisibilityRevertConverter}}">
<Grid Visibility="{Binding Archives.CurrentItem, Converter={StaticResource EmptyObjectToVisibilityRevertConverter}}">
<Border
HorizontalAlignment="Center"
VerticalAlignment="Center"

View File

@@ -23,7 +23,7 @@
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<shuxc:ScopedPage.Resources>
<shux:BindingProxy x:Key="BindingProxy" DataContext="{Binding}"/>

View File

@@ -23,7 +23,7 @@
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources>

View File

@@ -257,7 +257,7 @@
</Page.Resources>
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Grid Visibility="{Binding IsInitialized, Converter={StaticResource BoolToVisibilityConverter}}">

View File

@@ -24,7 +24,7 @@
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources>

View File

@@ -20,7 +20,7 @@
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources>

View File

@@ -23,7 +23,7 @@
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources>

View File

@@ -19,7 +19,7 @@
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources>

View File

@@ -18,7 +18,7 @@
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Grid x:Name="SettingPageGrid">

View File

@@ -23,7 +23,7 @@
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<shuxc:ScopedPage.Resources>
@@ -505,7 +505,7 @@
<PivotItem Header="{shuxm:ResourceString Name=ViewSpiralAbyssHutaoStatistics}">
<Grid DataContext="{Binding HutaoDatabaseViewModel}">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Grid.Resources>

View File

@@ -21,7 +21,7 @@
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources>
<shuxdc:VisibilityToObjectConverter

View File

@@ -20,7 +20,7 @@
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources>

View File

@@ -20,7 +20,7 @@
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources>

View File

@@ -14,7 +14,7 @@
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Grid x:Name="DragableGrid">

View File

@@ -178,7 +178,7 @@
</UserControl.Resources>
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<StackPanel
Margin="0,0,0,-2"

View File

@@ -12,7 +12,7 @@
<Grid Name="RootGrid" d:DataContext="{d:DesignInstance shvg:LaunchGameViewModel}">
<mxi:Interaction.Behaviors>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
<shuxb:InvokeCommandOnLoadedBehavior Command="{Binding LoadCommand}"/>
</mxi:Interaction.Behaviors>
<Grid.RowDefinitions>

View File

@@ -1,12 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.UI.Xaml;
namespace Snap.Hutao.ViewModel.Abstraction;
[HighQuality]
internal interface IViewModel : IPageScoped
internal interface IViewModel : IPageScoped, IResurrectable
{
CancellationToken CancellationToken { get; set; }
@@ -15,4 +16,6 @@ internal interface IViewModel : IPageScoped
IDeferContentLoader DeferContentLoader { get; set; }
bool IsViewDisposed { get; set; }
void Uninitialize();
}

View File

@@ -27,15 +27,28 @@ internal abstract partial class ViewModel : ObservableObject, IViewModel
public bool IsViewDisposed { get; set; }
protected TaskCompletionSource<bool> Initialization { get; } = new();
protected TaskCompletionSource<bool> Initialization { get; set; } = new();
[Command("OpenUICommand")]
protected virtual async Task OpenUIAsync()
public void Resurrect()
{
IsViewDisposed = false;
Initialization = new();
}
public void Uninitialize()
{
UninitializeOverride();
IsViewDisposed = true;
DeferContentLoader = default!;
}
[Command("LoadCommand")]
protected virtual async Task InitializeAsync()
{
try
{
// ConfigureAwait(true) sets value on UI thread
IsInitialized = await InitializeUIAsync().ConfigureAwait(true);
IsInitialized = await InitializeOverrideAsync().ConfigureAwait(true);
Initialization.TrySetResult(IsInitialized);
}
catch (OperationCanceledException)
@@ -43,12 +56,16 @@ internal abstract partial class ViewModel : ObservableObject, IViewModel
}
}
protected virtual ValueTask<bool> InitializeUIAsync()
protected virtual ValueTask<bool> InitializeOverrideAsync()
{
return ValueTask.FromResult(true);
}
protected async ValueTask<IDisposable> EnterCriticalExecutionAsync()
protected virtual void UninitializeOverride()
{
}
protected async ValueTask<IDisposable> EnterCriticalSectionAsync()
{
ThrowIfViewDisposed();
IDisposable disposable = await DisposeLock.EnterAsync(CancellationToken).ConfigureAwait(false);
@@ -100,17 +117,6 @@ internal abstract partial class ViewModel : ObservableObject, IViewModel
return false;
}
protected bool SetProperty<T>(ref T storage, T value, Func<T, ValueTask> changedAsyncCallback, [CallerMemberName] string? propertyName = null)
{
if (SetProperty(ref storage, value, propertyName))
{
changedAsyncCallback(value).SafeForget();
return true;
}
return false;
}
#endregion
private void ThrowIfViewDisposed()

View File

@@ -17,7 +17,7 @@ internal abstract partial class ViewModelSlim : ObservableObject
protected IServiceProvider ServiceProvider { get => serviceProvider; }
[Command("OpenUICommand")]
[Command("LoadCommand")]
protected virtual Task OpenUIAsync()
{
return Task.CompletedTask;

View File

@@ -17,34 +17,34 @@ namespace Snap.Hutao.ViewModel.Achievement;
[Injection(InjectAs.Scoped)]
internal sealed partial class AchievementImporter
{
private readonly AchievementImporterDependencies dependencies;
private readonly AchievementImporterScopeContext scopeContext;
public async ValueTask<bool> FromClipboardAsync()
public async ValueTask<bool> FromClipboardAsync(AchievementViewModelScopeContext context)
{
if (dependencies.AchievementService.CurrentArchive is not { } archive)
if (context.AchievementService.Archives.CurrentItem is not { } archive)
{
dependencies.InfoBarService.Warning(SH.ViewModelImportWarningTitle, SH.ViewModelImportWarningMessage2);
scopeContext.InfoBarService.Warning(SH.ViewModelImportWarningTitle, SH.ViewModelImportWarningMessage2);
return false;
}
if (await TryCatchGetUIAFFromClipboardAsync().ConfigureAwait(false) is not { } uiaf)
{
dependencies.InfoBarService.Warning(SH.ViewModelImportWarningTitle, SH.ViewModelImportWarningMessage);
scopeContext.InfoBarService.Warning(SH.ViewModelImportWarningTitle, SH.ViewModelImportWarningMessage);
return false;
}
return await TryImportAsync(archive, uiaf).ConfigureAwait(false);
return await TryImportCoreAsync(context, archive, uiaf).ConfigureAwait(false);
}
public async ValueTask<bool> FromFileAsync()
public async ValueTask<bool> FromFileAsync(AchievementViewModelScopeContext context)
{
if (dependencies.AchievementService.CurrentArchive is not { } archive)
if (context.AchievementService.Archives.CurrentItem is not { } archive)
{
dependencies.InfoBarService.Warning(SH.ViewModelImportWarningTitle, SH.ViewModelImportWarningMessage2);
scopeContext.InfoBarService.Warning(SH.ViewModelImportWarningTitle, SH.ViewModelImportWarningMessage2);
return false;
}
ValueResult<bool, ValueFile> pickerResult = dependencies.FileSystemPickerInteraction.PickFile(
ValueResult<bool, ValueFile> pickerResult = scopeContext.FileSystemPickerInteraction.PickFile(
SH.ServiceAchievementUIAFImportPickerTitile,
[(SH.ServiceAchievementUIAFImportPickerFilterText, "*.json")]);
@@ -53,39 +53,39 @@ internal sealed partial class AchievementImporter
return false;
}
ValueResult<bool, UIAF?> uiafResult = await file.DeserializeFromJsonAsync<UIAF>(dependencies.JsonSerializerOptions).ConfigureAwait(false);
ValueResult<bool, UIAF?> uiafResult = await file.DeserializeFromJsonAsync<UIAF>(scopeContext.JsonSerializerOptions).ConfigureAwait(false);
if (!uiafResult.TryGetValue(out UIAF? uiaf))
{
dependencies.InfoBarService.Warning(SH.ViewModelImportWarningTitle, SH.ViewModelImportWarningMessage);
scopeContext.InfoBarService.Warning(SH.ViewModelImportWarningTitle, SH.ViewModelImportWarningMessage);
return false;
}
return await TryImportAsync(archive, uiaf).ConfigureAwait(false);
return await TryImportCoreAsync(context, archive, uiaf).ConfigureAwait(false);
}
private async ValueTask<UIAF?> TryCatchGetUIAFFromClipboardAsync()
{
try
{
return await dependencies.ClipboardProvider.DeserializeFromJsonAsync<UIAF>().ConfigureAwait(false);
return await scopeContext.ClipboardProvider.DeserializeFromJsonAsync<UIAF>().ConfigureAwait(false);
}
catch (Exception ex)
{
dependencies.InfoBarService.Error(ex, SH.ViewModelImportFromClipboardErrorTitle);
scopeContext.InfoBarService.Error(ex, SH.ViewModelImportFromClipboardErrorTitle);
return null;
}
}
private async ValueTask<bool> TryImportAsync(EntityAchievementArchive archive, UIAF uiaf)
private async ValueTask<bool> TryImportCoreAsync(AchievementViewModelScopeContext context, EntityAchievementArchive archive, UIAF uiaf)
{
if (!uiaf.IsCurrentVersionSupported())
{
dependencies.InfoBarService.Warning(SH.ViewModelImportWarningTitle, SH.ViewModelAchievementImportWarningMessage);
scopeContext.InfoBarService.Warning(SH.ViewModelImportWarningTitle, SH.ViewModelAchievementImportWarningMessage);
return false;
}
AchievementImportDialog importDialog = await dependencies.ContentDialogFactory
AchievementImportDialog importDialog = await scopeContext.ContentDialogFactory
.CreateInstanceAsync<AchievementImportDialog>(uiaf).ConfigureAwait(false);
(bool isOk, ImportStrategyKind strategy) = await importDialog.GetImportStrategyAsync().ConfigureAwait(false);
@@ -94,18 +94,18 @@ internal sealed partial class AchievementImporter
return false;
}
await dependencies.TaskContext.SwitchToMainThreadAsync();
ContentDialog dialog = await dependencies.ContentDialogFactory
await scopeContext.TaskContext.SwitchToMainThreadAsync();
ContentDialog dialog = await scopeContext.ContentDialogFactory
.CreateForIndeterminateProgressAsync(SH.ViewModelAchievementImportProgress)
.ConfigureAwait(false);
ImportResult result;
using (await dialog.BlockAsync(dependencies.TaskContext).ConfigureAwait(false))
using (await dialog.BlockAsync(scopeContext.TaskContext).ConfigureAwait(false))
{
result = await dependencies.AchievementService.ImportFromUIAFAsync(archive, uiaf.List, strategy).ConfigureAwait(false);
result = await context.AchievementService.ImportFromUIAFAsync(archive, uiaf.List, strategy).ConfigureAwait(false);
}
dependencies.InfoBarService.Success($"{result}");
scopeContext.InfoBarService.Success($"{result}");
return true;
}
}

View File

@@ -11,12 +11,11 @@ namespace Snap.Hutao.ViewModel.Achievement;
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class AchievementImporterDependencies
internal sealed partial class AchievementImporterScopeContext
{
private readonly IFileSystemPickerInteraction fileSystemPickerInteraction;
private readonly JsonSerializerOptions jsonSerializerOptions;
private readonly IContentDialogFactory contentDialogFactory;
private readonly IAchievementService achievementService;
private readonly IClipboardProvider clipboardProvider;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
@@ -27,8 +26,6 @@ internal sealed partial class AchievementImporterDependencies
public IContentDialogFactory ContentDialogFactory { get => contentDialogFactory; }
public IAchievementService AchievementService { get => achievementService; }
public IClipboardProvider ClipboardProvider { get => clipboardProvider; }
public IInfoBarService InfoBarService { get => infoBarService; }

View File

@@ -1,7 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Collections;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.LifeCycle;
@@ -13,16 +15,12 @@ using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.UI.Xaml.Data;
using Snap.Hutao.UI.Xaml.View.Dialog;
using System.Collections.ObjectModel;
using System.Text.RegularExpressions;
using EntityAchievementArchive = Snap.Hutao.Model.Entity.AchievementArchive;
using MetadataAchievementGoal = Snap.Hutao.Model.Metadata.Achievement.AchievementGoal;
using SortDescription = CommunityToolkit.WinUI.Collections.SortDescription;
using SortDirection = CommunityToolkit.WinUI.Collections.SortDirection;
namespace Snap.Hutao.ViewModel.Achievement;
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INavigationRecipient
@@ -34,32 +32,31 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
private readonly SortDescription achievementDefaultSortDescription = new(nameof(AchievementView.Order), SortDirection.Ascending);
private readonly SortDescription achievementGoalDefaultSortDescription = new(nameof(AchievementGoalView.Order), SortDirection.Ascending);
private readonly AchievementViewModelDependencies dependencies;
private readonly AchievementViewModelScopeContext scopeContext;
private AdvancedCollectionView<AchievementView>? achievements;
private AdvancedCollectionView<AchievementGoalView>? achievementGoals;
private AchievementGoalView? selectedAchievementGoal;
private ObservableCollection<EntityAchievementArchive>? archives;
private EntityAchievementArchive? selectedArchive;
private AdvancedDbCollectionView<EntityAchievementArchive>? archives;
private bool isUncompletedItemsFirst = true;
private string searchText = string.Empty;
private string? finishDescription;
public ObservableCollection<EntityAchievementArchive>? Archives
public AdvancedDbCollectionView<EntityAchievementArchive>? Archives
{
get => archives;
set => SetProperty(ref archives, value);
}
public EntityAchievementArchive? SelectedArchive
{
get => selectedArchive;
set
{
if (SetProperty(ref selectedArchive, value))
if (archives is not null)
{
dependencies.AchievementService.CurrentArchive = value;
UpdateAchievementsAsync(value).SafeForget();
archives.CurrentChanged -= OnCurrentArchiveChanged;
}
SetProperty(ref archives, value);
if (value is not null)
{
value.CurrentChanged += OnCurrentArchiveChanged;
}
}
}
@@ -73,18 +70,18 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
public AdvancedCollectionView<AchievementGoalView>? AchievementGoals
{
get => achievementGoals;
set => SetProperty(ref achievementGoals, value);
}
public AchievementGoalView? SelectedAchievementGoal
{
get => selectedAchievementGoal;
set
{
if (SetProperty(ref selectedAchievementGoal, value))
if (achievementGoals is not null)
{
SearchText = string.Empty;
UpdateAchievementsFilterByGoal(value);
achievementGoals.CurrentChanged -= OnCurrentAchievementGoalChanged;
}
SetProperty(ref achievementGoals, value);
if (value is not null)
{
value.CurrentChanged += OnCurrentAchievementGoalChanged;
}
}
}
@@ -122,142 +119,165 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
return false;
}
protected override async ValueTask<bool> InitializeUIAsync()
protected override async ValueTask<bool> InitializeOverrideAsync()
{
if (!await dependencies.MetadataService.InitializeAsync().ConfigureAwait(false))
if (!await scopeContext.MetadataService.InitializeAsync().ConfigureAwait(false))
{
return false;
}
List<AchievementGoalView> sortedGoals;
ObservableCollection<EntityAchievementArchive> archives;
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
List<MetadataAchievementGoal> goals = await dependencies.MetadataService
List<MetadataAchievementGoal> goals = await scopeContext.MetadataService
.GetAchievementGoalListAsync(CancellationToken)
.ConfigureAwait(false);
sortedGoals = goals.SortBy(goal => goal.Order).SelectList(AchievementGoalView.From);
archives = dependencies.AchievementService.ArchiveCollection;
}
await dependencies.TaskContext.SwitchToMainThreadAsync();
await scopeContext.TaskContext.SwitchToMainThreadAsync();
AchievementGoals = new(sortedGoals, true);
Archives = archives;
SelectedArchive = dependencies.AchievementService.CurrentArchive;
Archives = scopeContext.AchievementService.Archives;
Archives.MoveCurrentTo(Archives.SourceCollection.SelectedOrDefault());
return true;
}
protected override void UninitializeOverride()
{
Archives?.Detach();
Archives = default;
AchievementGoals = default;
Achievements = default;
}
[GeneratedRegex("\\d\\.\\d")]
private static partial Regex VersionRegex();
private void OnCurrentArchiveChanged(object? sender, object? e)
{
UpdateAchievementsAsync(Archives?.CurrentItem).SafeForget(scopeContext.Logger);
}
private void OnCurrentAchievementGoalChanged(object? sender, object? e)
{
SearchText = string.Empty;
UpdateAchievementsFilterByGoal(AchievementGoals?.CurrentItem);
}
[Command("AddArchiveCommand")]
private async Task AddArchiveAsync()
{
if (Archives is not null)
if (Archives is null)
{
AchievementArchiveCreateDialog dialog = await dependencies.ContentDialogFactory.CreateInstanceAsync<AchievementArchiveCreateDialog>().ConfigureAwait(false);
(bool isOk, string name) = await dialog.GetInputAsync().ConfigureAwait(false);
return;
}
if (isOk)
{
ArchiveAddResultKind result = await dependencies.AchievementService.AddArchiveAsync(EntityAchievementArchive.From(name)).ConfigureAwait(false);
AchievementArchiveCreateDialog dialog = await scopeContext.ContentDialogFactory.CreateInstanceAsync<AchievementArchiveCreateDialog>().ConfigureAwait(false);
(bool isOk, string name) = await dialog.GetInputAsync().ConfigureAwait(false);
switch (result)
{
case ArchiveAddResultKind.Added:
await dependencies.TaskContext.SwitchToMainThreadAsync();
SelectedArchive = dependencies.AchievementService.CurrentArchive;
dependencies.InfoBarService.Success(SH.FormatViewModelAchievementArchiveAdded(name));
break;
case ArchiveAddResultKind.InvalidName:
dependencies.InfoBarService.Warning(SH.ViewModelAchievementArchiveInvalidName);
break;
case ArchiveAddResultKind.AlreadyExists:
dependencies.InfoBarService.Warning(SH.FormatViewModelAchievementArchiveAlreadyExists(name));
break;
default:
throw HutaoException.NotSupported();
}
}
if (!isOk)
{
return;
}
switch (await scopeContext.AchievementService.AddArchiveAsync(EntityAchievementArchive.From(name)).ConfigureAwait(false))
{
case ArchiveAddResultKind.Added:
await scopeContext.TaskContext.SwitchToMainThreadAsync();
scopeContext.InfoBarService.Success(SH.FormatViewModelAchievementArchiveAdded(name));
break;
case ArchiveAddResultKind.InvalidName:
scopeContext.InfoBarService.Warning(SH.ViewModelAchievementArchiveInvalidName);
break;
case ArchiveAddResultKind.AlreadyExists:
scopeContext.InfoBarService.Warning(SH.FormatViewModelAchievementArchiveAlreadyExists(name));
break;
default:
throw HutaoException.NotSupported();
}
}
[Command("RemoveArchiveCommand")]
private async Task RemoveArchiveAsync()
{
if (Archives is not null && SelectedArchive is not null)
if (Archives is null || !(Archives.CurrentItem is { } current))
{
string title = SH.FormatViewModelAchievementRemoveArchiveTitle(SelectedArchive.Name);
string content = SH.ViewModelAchievementRemoveArchiveContent;
ContentDialogResult result = await dependencies.ContentDialogFactory
.CreateForConfirmCancelAsync(title, content)
.ConfigureAwait(false);
return;
}
if (result == ContentDialogResult.Primary)
ContentDialogResult result = await scopeContext.ContentDialogFactory
.CreateForConfirmCancelAsync(
SH.FormatViewModelAchievementRemoveArchiveTitle(current.Name),
SH.ViewModelAchievementRemoveArchiveContent)
.ConfigureAwait(false);
if (result is not ContentDialogResult.Primary)
{
return;
}
try
{
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
try
{
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
{
await dependencies.AchievementService.RemoveArchiveAsync(SelectedArchive).ConfigureAwait(false);
}
// Re-select first archive
await dependencies.TaskContext.SwitchToMainThreadAsync();
SelectedArchive = Archives.FirstOrDefault();
}
catch (OperationCanceledException)
{
}
await scopeContext.AchievementService.RemoveArchiveAsync(current).ConfigureAwait(false);
}
await scopeContext.TaskContext.SwitchToMainThreadAsync();
Archives.MoveCurrentToFirst();
}
catch (OperationCanceledException)
{
}
}
[Command("ExportAsUIAFToFileCommand")]
private async Task ExportAsUIAFToFileAsync()
{
if (SelectedArchive is not null && Achievements is not null)
if (Archives?.CurrentItem is null || Achievements is null)
{
(bool isOk, ValueFile file) = dependencies.FileSystemPickerInteraction.SaveFile(
SH.ViewModelAchievementUIAFExportPickerTitle,
$"{dependencies.AchievementService.CurrentArchive?.Name}.json",
[(SH.ViewModelAchievementExportFileType, "*.json")]);
return;
}
if (isOk)
{
UIAF uiaf = await dependencies.AchievementService.ExportToUIAFAsync(SelectedArchive).ConfigureAwait(false);
if (await file.SerializeToJsonAsync(uiaf, dependencies.JsonSerializerOptions).ConfigureAwait(false))
{
dependencies.InfoBarService.Success(SH.ViewModelExportSuccessTitle, SH.ViewModelExportSuccessMessage);
}
else
{
dependencies.InfoBarService.Warning(SH.ViewModelExportWarningTitle, SH.ViewModelExportWarningMessage);
}
}
(bool isOk, ValueFile file) = scopeContext.FileSystemPickerInteraction.SaveFile(
SH.ViewModelAchievementUIAFExportPickerTitle,
$"{Archives.CurrentItem.Name}.json",
[(SH.ViewModelAchievementExportFileType, "*.json")]);
if (!isOk)
{
return;
}
UIAF uiaf = await scopeContext.AchievementService.ExportToUIAFAsync(Archives.CurrentItem).ConfigureAwait(false);
if (await file.SerializeToJsonAsync(uiaf, scopeContext.JsonSerializerOptions).ConfigureAwait(false))
{
scopeContext.InfoBarService.Success(SH.ViewModelExportSuccessTitle, SH.ViewModelExportSuccessMessage);
}
else
{
scopeContext.InfoBarService.Warning(SH.ViewModelExportWarningTitle, SH.ViewModelExportWarningMessage);
}
}
[Command("ImportUIAFFromClipboardCommand")]
private async Task ImportUIAFFromClipboardAsync()
{
if (await dependencies.AchievementImporter.FromClipboardAsync().ConfigureAwait(false))
if (await scopeContext.AchievementImporter.FromClipboardAsync(scopeContext).ConfigureAwait(false))
{
ArgumentNullException.ThrowIfNull(dependencies.AchievementService.CurrentArchive);
await UpdateAchievementsAsync(dependencies.AchievementService.CurrentArchive).ConfigureAwait(false);
await UpdateAchievementsAsync(Archives?.CurrentItem).ConfigureAwait(false);
}
}
[Command("ImportUIAFFromFileCommand")]
private async Task ImportUIAFFromFileAsync()
{
if (await dependencies.AchievementImporter.FromFileAsync().ConfigureAwait(false))
if (await scopeContext.AchievementImporter.FromFileAsync(scopeContext).ConfigureAwait(false))
{
ArgumentNullException.ThrowIfNull(dependencies.AchievementService.CurrentArchive);
await UpdateAchievementsAsync(dependencies.AchievementService.CurrentArchive).ConfigureAwait(false);
await UpdateAchievementsAsync(Archives?.CurrentItem).ConfigureAwait(false);
}
}
@@ -269,31 +289,32 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
return;
}
AchievementServiceMetadataContext context = await dependencies.MetadataService
AchievementServiceMetadataContext context = await scopeContext.MetadataService
.GetContextAsync<AchievementServiceMetadataContext>(CancellationToken)
.ConfigureAwait(false);
if (TryGetAchievements(archive, context, out List<AchievementView>? combined))
if (!TryGetAchievements(archive, context, out List<AchievementView>? combined))
{
await dependencies.TaskContext.SwitchToMainThreadAsync();
Achievements = new(combined, true);
UpdateAchievementsFinishPercent();
UpdateAchievementsFilterByGoal(SelectedAchievementGoal);
UpdateAchievementsSort();
return;
}
await scopeContext.TaskContext.SwitchToMainThreadAsync();
Achievements = new(combined, true);
AchievementFinishPercent.Update(this);
UpdateAchievementsFilterByGoal(AchievementGoals?.CurrentItem);
UpdateAchievementsSort();
}
private bool TryGetAchievements(EntityAchievementArchive archive, AchievementServiceMetadataContext context, [NotNullWhen(true)] out List<AchievementView>? combined)
{
try
{
combined = dependencies.AchievementService.GetAchievementViewList(archive, context);
combined = scopeContext.AchievementService.GetAchievementViewList(archive, context);
return true;
}
catch (HutaoException ex)
{
dependencies.InfoBarService.Error(ex);
scopeContext.InfoBarService.Error(ex);
combined = default;
return false;
}
@@ -340,45 +361,36 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
[Command("SearchAchievementCommand")]
private void UpdateAchievementsFilterBySearch(string? search)
{
if (Achievements is not null)
if (Achievements is null)
{
SetProperty(ref selectedAchievementGoal, null);
if (string.IsNullOrEmpty(search))
{
Achievements.Filter = default!;
return;
}
if (uint.TryParse(search, out uint achievementId))
{
Achievements.Filter = view => view.Inner.Id == achievementId;
return;
}
if (VersionRegex().IsMatch(search))
{
Achievements.Filter = view => view.Inner.Version == search;
return;
}
Achievements.Filter = view =>
{
return view.Inner.Title.Contains(search, StringComparison.CurrentCultureIgnoreCase)
|| view.Inner.Description.Contains(search, StringComparison.CurrentCultureIgnoreCase);
};
return;
}
}
private void UpdateAchievementsFinishPercent()
{
// 保存成就状态时,需要保持当前选择的成就分类
AchievementGoalView? currentSelectedAchievementGoal = SelectedAchievementGoal;
AchievementGoals?.MoveCurrentTo(default);
// 仅 读取成就列表 与 保存成就状态 时需要刷新成就进度
AchievementFinishPercent.Update(this);
if (string.IsNullOrEmpty(search))
{
Achievements.Filter = default!;
return;
}
SelectedAchievementGoal = currentSelectedAchievementGoal;
if (uint.TryParse(search, out uint achievementId))
{
Achievements.Filter = view => view.Inner.Id == achievementId;
return;
}
if (VersionRegex().IsMatch(search))
{
Achievements.Filter = view => view.Inner.Version == search;
return;
}
Achievements.Filter = view =>
{
return view.Inner.Title.Contains(search, StringComparison.CurrentCultureIgnoreCase)
|| view.Inner.Description.Contains(search, StringComparison.CurrentCultureIgnoreCase);
};
}
[Command("SaveAchievementCommand")]
@@ -386,8 +398,8 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav
{
if (achievement is not null)
{
dependencies.AchievementService.SaveAchievement(achievement);
UpdateAchievementsFinishPercent();
scopeContext.AchievementService.SaveAchievement(achievement);
AchievementFinishPercent.Update(this);
}
}
}

View File

@@ -11,19 +11,22 @@ namespace Snap.Hutao.ViewModel.Achievement;
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class AchievementViewModelDependencies
internal sealed partial class AchievementViewModelScopeContext
{
private readonly IFileSystemPickerInteraction fileSystemPickerInteraction;
private readonly ILogger<AchievementViewModelScopeContext> logger;
private readonly JsonSerializerOptions jsonSerializerOptions;
private readonly IContentDialogFactory contentDialogFactory;
private readonly AchievementImporter achievementImporter;
private readonly IAchievementService achievementService;
private readonly IMetadataService metadataService;
private readonly IInfoBarService infoBarService;
private readonly JsonSerializerOptions jsonSerializerOptions;
private readonly ITaskContext taskContext;
public IFileSystemPickerInteraction FileSystemPickerInteraction { get => fileSystemPickerInteraction; }
public ILogger<AchievementViewModelScopeContext> Logger { get => logger; }
public JsonSerializerOptions JsonSerializerOptions { get => jsonSerializerOptions; }
public IContentDialogFactory ContentDialogFactory { get => contentDialogFactory; }

View File

@@ -81,7 +81,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
}
}
protected override async ValueTask<bool> InitializeUIAsync()
protected override async ValueTask<bool> InitializeOverrideAsync()
{
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{
@@ -124,7 +124,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
try
{
ValueResult<RefreshResultKind, Summary?> summaryResult;
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
ContentDialog dialog = await contentDialogFactory
.CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyFetch)

View File

@@ -59,7 +59,7 @@ internal sealed partial class HutaoDatabaseViewModel : Abstraction.ViewModel
public Overview? Overview { get => overview; set => SetProperty(ref overview, value); }
/// <inheritdoc/>
protected override async Task OpenUIAsync()
protected override async Task InitializeAsync()
{
if (await hutaoCache.InitializeForSpiralAbyssViewAsync().ConfigureAwait(false))
{

View File

@@ -62,7 +62,7 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
public ObservableCollection<StatisticsCultivateItem>? StatisticsItems { get => statisticsItems; set => SetProperty(ref statisticsItems, value); }
protected override async ValueTask<bool> InitializeUIAsync()
protected override async ValueTask<bool> InitializeOverrideAsync()
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
@@ -189,7 +189,7 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
return;
}
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
ContentDialog dialog = await contentDialogFactory
.CreateForIndeterminateProgressAsync(SH.ViewModelCultivationRefreshInventoryProgress)

View File

@@ -58,7 +58,7 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel
/// </summary>
public ObservableCollection<DailyNoteEntry>? DailyNoteEntries { get => dailyNoteEntries; set => SetProperty(ref dailyNoteEntries, value); }
protected override async ValueTask<bool> InitializeUIAsync()
protected override async ValueTask<bool> InitializeOverrideAsync()
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
@@ -118,7 +118,7 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel
{
if (entry is not null)
{
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();

View File

@@ -48,7 +48,7 @@ internal sealed partial class FeedbackViewModel : Abstraction.ViewModel
public IPInformation? IPInformation { get => ipInformation; private set => SetProperty(ref ipInformation, value); }
protected override async ValueTask<bool> InitializeUIAsync()
protected override async ValueTask<bool> InitializeOverrideAsync()
{
Response<IPInformation> resp = await hutaoInfrastructureClient.GetIPInformationAsync().ConfigureAwait(false);
IPInformation info = resp.IsOk() ? resp.Data : IPInformation.Default;

View File

@@ -95,7 +95,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
/// </summary>
public HutaoCloudStatisticsViewModel HutaoCloudStatisticsViewModel { get => hutaoCloudStatisticsViewModel; }
protected override async ValueTask<bool> InitializeUIAsync()
protected override async ValueTask<bool> InitializeOverrideAsync()
{
try
{
@@ -104,7 +104,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
ArgumentNullException.ThrowIfNull(gachaLogService.ArchiveCollection);
ObservableCollection<GachaArchive> archives = gachaLogService.ArchiveCollection;
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
await taskContext.SwitchToMainThreadAsync();
Archives = archives;
@@ -173,7 +173,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
try
{
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
try
{
@@ -278,7 +278,7 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
if (result == ContentDialogResult.Primary)
{
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
await gachaLogService.RemoveArchiveAsync(SelectedArchive).ConfigureAwait(false);

View File

@@ -65,7 +65,7 @@ internal sealed partial class HutaoCloudViewModel : Abstraction.ViewModel
}
}
protected override async ValueTask<bool> InitializeUIAsync()
protected override async ValueTask<bool> InitializeOverrideAsync()
{
await hutaoUserService.InitializeAsync().ConfigureAwait(false);
await RefreshUidCollectionAsync().ConfigureAwait(false);

View File

@@ -95,7 +95,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
{
try
{
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
LaunchScheme? scheme = launchGameShared.GetCurrentLaunchSchemeFromConfigFile();
@@ -170,7 +170,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
SelectedGamePathEntry = selectedEntry;
}
protected override ValueTask<bool> InitializeUIAsync()
protected override ValueTask<bool> InitializeOverrideAsync()
{
ImmutableList<GamePathEntry> gamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
SetGamePathEntriesAndSelectedGamePathEntry(gamePathEntries, entry);

View File

@@ -167,7 +167,7 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel
set => SetProperty(ref downloadSummaries, value);
}
protected override async ValueTask<bool> InitializeUIAsync()
protected override async ValueTask<bool> InitializeOverrideAsync()
{
HutaoInfrastructureClient hutaoInfrastructureClient = serviceProvider.GetRequiredService<HutaoInfrastructureClient>();
HutaoResponse<StaticResourceSizeInformation> response = await hutaoInfrastructureClient.GetStaticSizeAsync().ConfigureAwait(false);

View File

@@ -51,7 +51,7 @@ internal sealed partial class AnnouncementViewModel : Abstraction.ViewModel
public List<CardReference>? Cards { get => cards; set => SetProperty(ref cards, value); }
protected override ValueTask<bool> InitializeUIAsync()
protected override ValueTask<bool> InitializeOverrideAsync()
{
InitializeDashboard();
InitializeInGameAnnouncementAsync().SafeForget();

View File

@@ -216,7 +216,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
}
}
protected override ValueTask<bool> InitializeUIAsync()
protected override ValueTask<bool> InitializeOverrideAsync()
{
CacheFolderView = new(taskContext, runtimeOptions.LocalCache);
DataFolderView = new(taskContext, runtimeOptions.DataFolder);

View File

@@ -67,7 +67,7 @@ internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel
}
}
protected override async ValueTask<bool> InitializeUIAsync()
protected override async ValueTask<bool> InitializeOverrideAsync()
{
if (await spiralAbyssRecordService.InitializeAsync().ConfigureAwait(false))
{
@@ -90,7 +90,7 @@ internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel
ObservableCollection<SpiralAbyssView>? collection = null;
try
{
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
collection = await spiralAbyssRecordService
.GetSpiralAbyssViewCollectionAsync(userAndUid)
@@ -115,7 +115,7 @@ internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel
{
try
{
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
await spiralAbyssRecordService
.RefreshSpiralAbyssAsync(userAndUid)

View File

@@ -56,7 +56,7 @@ internal sealed partial class TitleViewModel : Abstraction.ViewModel
public UpdateStatus? UpdateStatus { get => updateStatus; set => SetProperty(ref updateStatus, value); }
protected override async ValueTask<bool> InitializeUIAsync()
protected override async ValueTask<bool> InitializeOverrideAsync()
{
await DoCheckUpdateAsync().ConfigureAwait(false);
return true;

View File

@@ -90,7 +90,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
public FrozenDictionary<string, SearchToken>? AvailableTokens { get => availableTokens; }
protected override async ValueTask<bool> InitializeUIAsync()
protected override async ValueTask<bool> InitializeOverrideAsync()
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
@@ -108,7 +108,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
await CombineComplexDataAsync(list, idMaterialMap).ConfigureAwait(false);
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
await taskContext.SwitchToMainThreadAsync();
Avatars = new(list, true);

View File

@@ -49,7 +49,7 @@ internal sealed partial class WikiMonsterViewModel : Abstraction.ViewModel
/// </summary>
public BaseValueInfo? BaseValueInfo { get => baseValueInfo; set => SetProperty(ref baseValueInfo, value); }
protected override async ValueTask<bool> InitializeUIAsync()
protected override async ValueTask<bool> InitializeOverrideAsync()
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
@@ -66,7 +66,7 @@ internal sealed partial class WikiMonsterViewModel : Abstraction.ViewModel
List<Monster> ordered = monsters.SortBy(m => m.RelationshipId.Value);
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
await taskContext.SwitchToMainThreadAsync();
Monsters = new(ordered, true);

View File

@@ -90,7 +90,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
public FrozenDictionary<string, SearchToken>? AvailableTokens { get => availableTokens; }
/// <inheritdoc/>
protected override async Task OpenUIAsync()
protected override async Task InitializeAsync()
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
@@ -107,7 +107,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
await CombineComplexDataAsync(list, idMaterialMap).ConfigureAwait(false);
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
await taskContext.SwitchToMainThreadAsync();