Merge pull request #1356 from DGP-Studio/feat/reorder

This commit is contained in:
DismissedLight
2024-02-03 21:50:20 +08:00
committed by GitHub
16 changed files with 157 additions and 58 deletions

View File

@@ -1,25 +1,29 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Collections;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity.Database;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Core.Database;
internal sealed class ObservableReorderableDbCollection<T> : ObservableCollection<T>
where T : class, IReorderable
internal sealed class ObservableReorderableDbCollection<TEntity> : ObservableCollection<TEntity>
where TEntity : class, IReorderable
{
private readonly DbContext dbContext;
private bool previousChangeIsRemoved;
private readonly IServiceProvider serviceProvider;
public ObservableReorderableDbCollection(List<T> items, DbContext dbContext)
: base(AdjustIndex(items))
public ObservableReorderableDbCollection(List<TEntity> items, IServiceProvider serviceProvider)
: base(AdjustIndex(items.SortBy(x => x.Index)))
{
this.dbContext = dbContext;
this.serviceProvider = serviceProvider;
}
public IAdvancedCollectionView? View { get; set; }
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
@@ -27,26 +31,18 @@ internal sealed class ObservableReorderableDbCollection<T> : ObservableCollectio
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove:
previousChangeIsRemoved = true;
break;
case NotifyCollectionChangedAction.Add:
if (!previousChangeIsRemoved)
{
return;
}
OnReorder();
previousChangeIsRemoved = false;
break;
}
}
private static List<T> AdjustIndex(List<T> list)
private static List<TEntity> AdjustIndex(List<TEntity> list)
{
Span<T> span = CollectionsMarshal.AsSpan(list);
Span<TEntity> span = CollectionsMarshal.AsSpan(list);
for (int i = 0; i < list.Count; i++)
{
ref readonly T item = ref span[i];
ref readonly TEntity item = ref span[i];
item.Index = i;
}
@@ -55,12 +51,79 @@ internal sealed class ObservableReorderableDbCollection<T> : ObservableCollectio
private void OnReorder()
{
AdjustIndex((List<T>)Items);
DbSet<T> dbSet = dbContext.Set<T>();
foreach (ref readonly T item in CollectionsMarshal.AsSpan((List<T>)Items))
using (View?.DeferRefresh())
{
dbSet.UpdateAndSave(item);
AdjustIndex((List<TEntity>)Items);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
foreach (ref readonly TEntity item in CollectionsMarshal.AsSpan((List<TEntity>)Items))
{
dbSet.UpdateAndSave(item);
}
}
}
}
}
[SuppressMessage("", "SA1402")]
internal sealed class ObservableReorderableDbCollection<TEntityOnly, TEntity> : ObservableCollection<TEntityOnly>
where TEntityOnly : class, IEntityOnly<TEntity>
where TEntity : class, IReorderable
{
private readonly IServiceProvider serviceProvider;
public ObservableReorderableDbCollection(List<TEntityOnly> items, IServiceProvider serviceProvider)
: base(AdjustIndex(items.SortBy(x => x.Entity.Index)))
{
this.serviceProvider = serviceProvider;
}
public IAdvancedCollectionView? View { get; set; }
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Add:
OnReorder();
break;
}
}
private static List<TEntityOnly> AdjustIndex(List<TEntityOnly> list)
{
Span<TEntityOnly> span = CollectionsMarshal.AsSpan(list);
for (int i = 0; i < list.Count; i++)
{
ref readonly TEntityOnly item = ref span[i];
item.Entity.Index = i;
}
return list;
}
private void OnReorder()
{
using (View?.DeferRefresh())
{
AdjustIndex((List<TEntityOnly>)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))
{
dbSet.UpdateAndSave(item.Entity);
}
}
}
}
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
using System.Text;
@@ -113,6 +115,25 @@ internal static partial class EnumerableExtension
return new ObservableCollection<T>(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ObservableReorderableDbCollection<TEntity> ToObservableReorderableDbCollection<TEntity>(this IEnumerable<TEntity> source, IServiceProvider serviceProvider)
where TEntity : class, IReorderable
{
return source is List<TEntity> list
? new ObservableReorderableDbCollection<TEntity>(list, serviceProvider)
: new ObservableReorderableDbCollection<TEntity>([.. source], serviceProvider);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ObservableReorderableDbCollection<TEntityOnly, TEntity> ToObservableReorderableDbCollection<TEntityOnly, TEntity>(this IEnumerable<TEntityOnly> source, IServiceProvider serviceProvider)
where TEntityOnly : class, IEntityOnly<TEntity>
where TEntity : class, IReorderable
{
return source is List<TEntityOnly> list
? new ObservableReorderableDbCollection<TEntityOnly, TEntity>(list, serviceProvider)
: new ObservableReorderableDbCollection<TEntityOnly, TEntity>([.. source], serviceProvider);
}
/// <summary>
/// Concatenates each element from the collection into single string.
/// </summary>

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Model.Entity;
@@ -18,9 +19,9 @@ internal sealed partial class GameAccountService : IGameAccountService
private readonly IGameDbService gameDbService;
private readonly ITaskContext taskContext;
private ObservableCollection<GameAccount>? gameAccounts;
private ObservableReorderableDbCollection<GameAccount>? gameAccounts;
public ObservableCollection<GameAccount> GameAccountCollection
public ObservableReorderableDbCollection<GameAccount> GameAccountCollection
{
get => gameAccounts ??= gameDbService.GetGameAccountCollection();
}

View File

@@ -1,15 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game.Account;
internal interface IGameAccountService
{
ObservableCollection<GameAccount> GameAccountCollection { get; }
ObservableReorderableDbCollection<GameAccount> GameAccountCollection { get; }
void AttachGameAccountToUid(GameAccount gameAccount, string uid);

View File

@@ -5,7 +5,6 @@ using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game;
@@ -15,12 +14,12 @@ internal sealed partial class GameDbService : IGameDbService
{
private readonly IServiceProvider serviceProvider;
public ObservableCollection<GameAccount> GetGameAccountCollection()
public ObservableReorderableDbCollection<GameAccount> GetGameAccountCollection()
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.GameAccounts.AsNoTracking().ToObservableCollection();
return appDbContext.GameAccounts.AsNoTracking().ToObservableReorderableDbCollection(serviceProvider);
}
}

View File

@@ -1,13 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.Game.Account;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Launching.Handler;
using Snap.Hutao.Service.Game.PathAbstraction;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game;
@@ -24,7 +24,7 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
private readonly IGamePathService gamePathService;
/// <inheritdoc/>
public ObservableCollection<GameAccount> GameAccountCollection
public ObservableReorderableDbCollection<GameAccount> GameAccountCollection
{
get => gameAccountService.GameAccountCollection;
}

View File

@@ -1,8 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game;
@@ -12,7 +12,7 @@ internal interface IGameDbService
ValueTask RemoveGameAccountByIdAsync(Guid id);
ObservableCollection<GameAccount> GetGameAccountCollection();
ObservableReorderableDbCollection<GameAccount> GetGameAccountCollection();
void UpdateGameAccount(GameAccount gameAccount);

View File

@@ -1,10 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.Game.Configuration;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game;
@@ -17,7 +17,7 @@ internal interface IGameServiceFacade
/// <summary>
/// 游戏内账号集合
/// </summary>
ObservableCollection<GameAccount> GameAccountCollection { get; }
ObservableReorderableDbCollection<GameAccount> GameAccountCollection { get; }
/// <summary>
/// 将账号绑定到对应的Uid

View File

@@ -1,10 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.ViewModel.User.User;
using EntityUser = Snap.Hutao.Model.Entity.User;
namespace Snap.Hutao.Service.User;
@@ -14,7 +16,7 @@ internal interface IUserCollectionService
ValueTask<ObservableCollection<UserAndUid>> GetUserAndUidCollectionAsync();
ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync();
ValueTask<ObservableReorderableDbCollection<BindingUser, EntityUser>> GetUserCollectionAsync();
UserGameRole? GetUserGameRoleByUid(string uid);

View File

@@ -1,10 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.ViewModel.User.User;
using EntityUser = Snap.Hutao.Model.Entity.User;
namespace Snap.Hutao.Service.User;
@@ -32,7 +34,7 @@ internal interface IUserService
/// 此操作不能取消
/// </summary>
/// <returns>准备完成的用户信息集合</returns>
ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync();
ValueTask<ObservableReorderableDbCollection<BindingUser, EntityUser>> GetUserCollectionAsync();
/// <summary>
/// 获取角色信息

View File

@@ -9,6 +9,7 @@ using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.ViewModel.User.User;
using EntityUser = Snap.Hutao.Model.Entity.User;
namespace Snap.Hutao.Service.User;
@@ -18,13 +19,14 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
{
private readonly ScopedDbCurrent<BindingUser, Model.Entity.User, UserChangedMessage> dbCurrent;
private readonly IUserInitializationService userInitializationService;
private readonly IServiceProvider serviceProvider;
private readonly IUserDbService userDbService;
private readonly ITaskContext taskContext;
private readonly IMessenger messenger;
private readonly SemaphoreSlim throttler = new(1);
private ObservableCollection<BindingUser>? userCollection;
private ObservableReorderableDbCollection<BindingUser, EntityUser>? userCollection;
private Dictionary<string, BindingUser>? midUserMap;
private ObservableCollection<UserAndUid>? userAndUidCollection;
@@ -36,7 +38,7 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
set => dbCurrent.Current = value;
}
public async ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync()
public async ValueTask<ObservableReorderableDbCollection<BindingUser, EntityUser>> GetUserCollectionAsync()
{
// Force run in background thread, otherwise will cause reentrance
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
@@ -62,7 +64,7 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
}
}
userCollection = users.ToObservableCollection();
userCollection = users.ToObservableReorderableDbCollection<BindingUser, EntityUser>(serviceProvider);
try
{

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab;
@@ -9,6 +10,7 @@ using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Response;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.ViewModel.User.User;
using EntityUser = Snap.Hutao.Model.Entity.User;
namespace Snap.Hutao.Service.User;
@@ -41,7 +43,7 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
await userDbService.RemoveUsersAsync().ConfigureAwait(false);
}
public ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync()
public ValueTask<ObservableReorderableDbCollection<BindingUser, EntityUser>> GetUserCollectionAsync()
{
return userCollectionService.GetUserCollectionAsync();
}

View File

@@ -202,6 +202,8 @@
IsClickEnabled="True"/>
<Border Padding="0,1" Style="{StaticResource BorderCardStyle}">
<ListView
AllowDrop="True"
CanReorderItems="True"
ItemTemplate="{StaticResource GameAccountListTemplate}"
ItemsSource="{Binding GameAccountsView}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>

View File

@@ -261,6 +261,8 @@
<ListView
Grid.Row="1"
Margin="4"
AllowDrop="True"
CanReorderItems="True"
ItemsSource="{Binding Users}"
SelectedItem="{Binding SelectedUser, Mode=TwoWay}"
SelectionMode="Single">

View File

@@ -5,6 +5,7 @@ using CommunityToolkit.WinUI.Collections;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.UI.Windowing;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics.CodeAnalysis;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
@@ -18,7 +19,6 @@ using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
namespace Snap.Hutao.ViewModel.Game;
@@ -241,6 +241,8 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
{
await taskContext.SwitchToMainThreadAsync();
SelectedGameAccount = account;
await UpdateGameAccountsViewAsync().ConfigureAwait(false);
}
}
catch (UserdataCorruptedException ex)
@@ -326,18 +328,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
GameResource = response.Data;
}
}
async ValueTask UpdateGameAccountsViewAsync()
{
gameAccountFilter = new(SelectedScheme?.GetSchemeType());
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
await taskContext.SwitchToMainThreadAsync();
GameAccountsView = new(accounts, true)
{
Filter = gameAccountFilter.Filter,
};
}
}
[Command("IdentifyMonitorsCommand")]
@@ -363,4 +353,16 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
window.Close();
}
}
private async ValueTask UpdateGameAccountsViewAsync()
{
gameAccountFilter = new(SelectedScheme?.GetSchemeType());
ObservableReorderableDbCollection<GameAccount> accounts = gameService.GameAccountCollection;
await taskContext.SwitchToMainThreadAsync();
GameAccountsView = new(accounts, true)
{
Filter = gameAccountFilter.Filter,
};
}
}

View File

@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO.DataTransfer;
using Snap.Hutao.Factory.ContentDialog;
@@ -17,8 +18,8 @@ using Snap.Hutao.View.Page;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Passport;
using Snap.Hutao.Web.Response;
using System.Collections.ObjectModel;
using System.Text;
using EntityUser = Snap.Hutao.Model.Entity.User;
namespace Snap.Hutao.ViewModel.User;
@@ -40,7 +41,7 @@ internal sealed partial class UserViewModel : ObservableObject
private readonly IUserService userService;
private User? selectedUser;
private ObservableCollection<User>? users;
private ObservableReorderableDbCollection<User, EntityUser>? users;
/// <summary>
/// 当前选择的用户信息
@@ -66,7 +67,7 @@ internal sealed partial class UserViewModel : ObservableObject
/// <summary>
/// 用户信息集合
/// </summary>
public ObservableCollection<User>? Users { get => users; set => SetProperty(ref users, value); }
public ObservableReorderableDbCollection<User, EntityUser>? Users { get => users; set => SetProperty(ref users, value); }
/// <summary>
/// 处理用户操作结果