make users great again

This commit is contained in:
DismissedLight
2024-07-01 17:00:18 +08:00
parent d97bd4fd79
commit 17d27f9535
74 changed files with 581 additions and 1451 deletions

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database.Abstraction;
using Snap.Hutao.Model;
@@ -53,10 +52,6 @@ internal sealed class AdvancedDbCollectionView<TEntity> : AdvancedCollectionView
dbContext.Set<TEntity>().UpdateAndSave(currentItem);
}
}
serviceProvider
.GetRequiredService<IMessenger>()
.Send(new AdvancedDbCollectionViewCurrentChangedMessage<TEntity>(currentItem));
}
}
@@ -105,9 +100,5 @@ internal sealed class AdvancedDbCollectionView<TEntityAccess, TEntity> : Advance
dbContext.Set<TEntity>().UpdateAndSave(currentItem.Entity);
}
}
serviceProvider
.GetRequiredService<IMessenger>()
.Send(new AdvancedDbCollectionViewCurrentChangedMessage<TEntityAccess>(currentItem));
}
}

View File

@@ -1,15 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Database;
internal sealed class AdvancedDbCollectionViewCurrentChangedMessage<TItem>
where TItem : class
{
public AdvancedDbCollectionViewCurrentChangedMessage(TItem? currentItem)
{
CurrentItem = currentItem;
}
public TItem? CurrentItem { get; }
}

View File

@@ -20,14 +20,6 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
[Obsolete("Async operation over sqlite is meaningless")]
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
where TEntity : class
{
dbSet.Add(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
}
public static int AddRangeAndSave<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class
{
@@ -35,14 +27,6 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
[Obsolete("Async operation over sqlite is meaningless")]
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities, CancellationToken token = default)
where TEntity : class
{
dbSet.AddRange(entities);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
}
public static int RemoveAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -50,14 +34,6 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
[Obsolete("Async operation over sqlite is meaningless")]
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
where TEntity : class
{
dbSet.Remove(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
}
public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -65,14 +41,6 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
[Obsolete("Async operation over sqlite is meaningless")]
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
where TEntity : class
{
dbSet.Update(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int SaveChangesAndClearChangeTracker<TEntity>(this DbSet<TEntity> dbSet)
where TEntity : class
@@ -83,17 +51,6 @@ internal static class DbSetExtension
return count;
}
[Obsolete("Async operation over sqlite is meaningless")]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet, CancellationToken token = default)
where TEntity : class
{
DbContext dbContext = dbSet.Context();
int count = await dbContext.SaveChangesAsync(token).ConfigureAwait(false);
dbContext.ChangeTracker.Clear();
return count;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static DbContext Context<TEntity>(this DbSet<TEntity> dbSet)
where TEntity : class

View File

@@ -1,124 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database.Abstraction;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity.Database;
namespace Snap.Hutao.Core.Database;
[Obsolete]
[ConstructorGenerated]
internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TEntity>, new()
{
private readonly IServiceProvider serviceProvider;
private readonly IMessenger messenger;
private TEntity? current;
public TEntity? Current
{
get => current;
set
{
// prevent useless sets
if (current == value)
{
return;
}
if (serviceProvider.IsDisposed())
{
return;
}
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
// only update when not processing a deletion
if (value is not null && current is not null)
{
current.IsSelected = false;
dbSet.UpdateAndSave(current);
}
TMessage message = new() { OldValue = current, NewValue = value };
current = value;
if (current is not null)
{
current.IsSelected = true;
dbSet.UpdateAndSave(current);
}
messenger.Send(message);
}
}
}
}
[Obsolete]
[ConstructorGenerated]
internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
where TEntityOnly : class, IEntityAccess<TEntity>
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TEntityOnly>, new()
{
private readonly IServiceProvider serviceProvider;
private readonly IMessenger messenger;
private TEntityOnly? current;
public TEntityOnly? Current
{
get => current;
set
{
// prevent useless sets
if (current == value)
{
return;
}
if (serviceProvider.IsDisposed())
{
return;
}
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
// only update when not processing a deletion
if (value is not null)
{
if (current is not null)
{
current.Entity.IsSelected = false;
dbSet.UpdateAndSave(current.Entity);
}
}
TMessage message = new() { OldValue = current, NewValue = value };
current = value;
if (current is not null)
{
current.Entity.IsSelected = true;
dbSet.UpdateAndSave(current.Entity);
}
messenger.Send(message);
}
}
}
}

View File

@@ -44,7 +44,7 @@ internal static class DependencyInjection
.AddConfiguredHttpClients()
// Discrete services
.AddSingleton<IMessenger, StrongReferenceMessenger>()
.AddSingleton<IMessenger, WeakReferenceMessenger>()
.BuildServiceProvider(true);
Ioc.Default.ConfigureServices(serviceProvider);

View File

@@ -30,10 +30,7 @@ internal static class IocConfiguration
/// <returns>可继续操作的集合</returns>
public static IServiceCollection AddDatabase(this IServiceCollection services)
{
return services
.AddTransient(typeof(Database.ScopedDbCurrent<,>))
.AddTransient(typeof(Database.ScopedDbCurrent<,,>))
.AddDbContextPool<AppDbContext>(AddDbContextCore);
return services.AddDbContextPool<AppDbContext>(AddDbContextCore);
static void AddDbContextCore(IServiceProvider serviceProvider, DbContextOptionsBuilder builder)
{

View File

@@ -4,6 +4,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Database.Abstraction;
using Snap.Hutao.Model.Entity.Abstraction;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.UI.Xaml.Data;
using System.ComponentModel.DataAnnotations;
@@ -17,6 +18,7 @@ namespace Snap.Hutao.Model.Entity;
[HighQuality]
[Table("game_accounts")]
internal sealed class GameAccount : ObservableObject,
IAppDbEntity,
IReorderable,
IAdvancedCollectionViewItem,
IMappingFrom<GameAccount, string, string, SchemeType>

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity.Abstraction;
namespace Snap.Hutao.Service.Abstraction;
@@ -20,27 +19,9 @@ internal static class AppDbServiceAppDbEntityExtension
return service.Delete(e => e.InnerId == innerId);
}
public static ValueTask<int> DeleteByInnerIdAsync<TEntity>(this IAppDbService<TEntity> service, TEntity entity, CancellationToken token = default)
where TEntity : class, IAppDbEntity
{
return service.DeleteByInnerIdAsync(entity.InnerId, token);
}
public static ValueTask<int> DeleteByInnerIdAsync<TEntity>(this IAppDbService<TEntity> service, Guid innerId, CancellationToken token = default)
where TEntity : class, IAppDbEntity
{
return service.DeleteAsync(e => e.InnerId == innerId, token);
}
public static List<TEntity> ListByArchiveId<TEntity>(this IAppDbService<TEntity> service, Guid archiveId)
where TEntity : class, IAppDbEntityHasArchive
{
return service.Query(query => query.Where(e => e.ArchiveId == archiveId).ToList());
}
public static ValueTask<List<TEntity>> ListByArchiveIdAsync<TEntity>(this IAppDbService<TEntity> service, Guid archiveId, CancellationToken token = default)
where TEntity : class, IAppDbEntityHasArchive
{
return service.QueryAsync((query, token) => query.Where(e => e.ArchiveId == archiveId).ToListAsync(token), token);
}
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using System.Collections.ObjectModel;
using System.Linq.Expressions;
@@ -27,24 +26,6 @@ internal static class AppDbServiceCollectionExtension
return service.Query(query1 => query(query1).ToList());
}
public static ValueTask<List<TEntity>> ListAsync<TEntity>(this IAppDbService<TEntity> service, CancellationToken token = default)
where TEntity : class
{
return service.QueryAsync((query, token) => query.ToListAsync(token), token);
}
public static ValueTask<List<TEntity>> ListAsync<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate, CancellationToken token = default)
where TEntity : class
{
return service.ListAsync(query => query.Where(predicate), token);
}
public static ValueTask<List<TResult>> ListAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, IQueryable<TResult>> query, CancellationToken token = default)
where TEntity : class
{
return service.QueryAsync((query1, token) => query(query1).ToListAsync(token), token);
}
public static ObservableCollection<TEntity> ObservableCollection<TEntity>(this IAppDbService<TEntity> service)
where TEntity : class
{

View File

@@ -20,137 +20,46 @@ internal static class AppDbServiceExtension
}
}
public static async ValueTask<TResult> ExecuteAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<DbSet<TEntity>, ValueTask<TResult>> asyncFunc)
where TEntity : class
{
using (IServiceScope scope = service.ServiceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
return await asyncFunc(appDbContext.Set<TEntity>()).ConfigureAwait(false);
}
}
public static async ValueTask<TResult> ExecuteAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<DbSet<TEntity>, CancellationToken, ValueTask<TResult>> asyncFunc, CancellationToken token)
where TEntity : class
{
using (IServiceScope scope = service.ServiceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
return await asyncFunc(appDbContext.Set<TEntity>(), token).ConfigureAwait(false);
}
}
public static async ValueTask<TResult> ExecuteAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<DbSet<TEntity>, Task<TResult>> asyncFunc)
where TEntity : class
{
using (IServiceScope scope = service.ServiceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
return await asyncFunc(appDbContext.Set<TEntity>()).ConfigureAwait(false);
}
}
public static async ValueTask<TResult> ExecuteAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<DbSet<TEntity>, CancellationToken, Task<TResult>> asyncFunc, CancellationToken token)
where TEntity : class
{
using (IServiceScope scope = service.ServiceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
return await asyncFunc(appDbContext.Set<TEntity>(), token).ConfigureAwait(false);
}
}
public static int Add<TEntity>(this IAppDbService<TEntity> service, TEntity entity)
where TEntity : class
{
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
{
return service.ExecuteAsync((dbset, token) => dbset.AddAndSaveAsync(entity, token), token);
}
public static int AddRange<TEntity>(this IAppDbService<TEntity> service, IEnumerable<TEntity> entities)
where TEntity : class
{
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
{
return service.ExecuteAsync((dbset, token) => dbset.AddRangeAndSaveAsync(entities, token), token);
}
public static TResult Query<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, TResult> func)
where TEntity : class
{
return service.Execute(dbset => func(dbset.AsNoTracking()));
}
public static ValueTask<TResult> QueryAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, ValueTask<TResult>> func)
where TEntity : class
{
return service.ExecuteAsync(dbset => func(dbset.AsNoTracking()));
}
public static ValueTask<TResult> QueryAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, CancellationToken, ValueTask<TResult>> func, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => func(dbset.AsNoTracking(), token), token);
}
public static ValueTask<TResult> QueryAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, Task<TResult>> func)
where TEntity : class
{
return service.ExecuteAsync(dbset => func(dbset.AsNoTracking()));
}
public static ValueTask<TResult> QueryAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, CancellationToken, Task<TResult>> func, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => func(dbset.AsNoTracking(), token), token);
}
public static TEntity Single<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate)
where TEntity : class
{
return service.Query(query => query.Single(predicate));
}
public static ValueTask<TEntity> SingleAsync<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate, CancellationToken token = default)
where TEntity : class
{
return service.QueryAsync((query, token) => query.SingleAsync(predicate, token), token);
}
public static TEntity? SingleOrDefault<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate)
where TEntity : class
{
return service.Query(query => query.SingleOrDefault(predicate));
}
public static ValueTask<TEntity?> SingleOrDefaultAsync<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate, CancellationToken token = default)
where TEntity : class
{
return service.QueryAsync((query, token) => query.SingleOrDefaultAsync(predicate, token), token);
}
public static int Update<TEntity>(this IAppDbService<TEntity> service, TEntity entity)
where TEntity : class
{
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
public static int Delete<TEntity>(this IAppDbService<TEntity> service)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => dbset.UpdateAndSaveAsync(entity, token), token);
return service.Execute(dbset => dbset.ExecuteDelete());
}
public static int Delete<TEntity>(this IAppDbService<TEntity> service, TEntity entity)
@@ -164,17 +73,4 @@ 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
{
return service.ExecuteAsync((dbset, token) => dbset.RemoveAndSaveAsync(entity, token), token);
}
public static ValueTask<int> DeleteAsync<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => dbset.Where(predicate).ExecuteDeleteAsync(token), token);
}
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Primitive;
@@ -33,26 +32,21 @@ internal sealed partial class AchievementDbService : IAchievementDbService
}
}
public ValueTask<int> GetFinishedAchievementCountByArchiveIdAsync(Guid archiveId, CancellationToken token = default)
public int GetFinishedAchievementCountByArchiveId(Guid archiveId)
{
return this.QueryAsync<EntityAchievement, int>(
(query, token) => query
.Where(a => a.ArchiveId == archiveId)
.Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
.CountAsync(token),
token);
return this.Query<EntityAchievement, int>(query => query
.Where(a => a.ArchiveId == archiveId)
.Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
.Count());
}
[SuppressMessage("", "CA1305")]
public ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take, CancellationToken token = default)
public List<EntityAchievement> GetLatestFinishedAchievementListByArchiveId(Guid archiveId, int take)
{
return this.ListAsync<EntityAchievement, EntityAchievement>(
query => query
.Where(a => a.ArchiveId == archiveId)
.Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
.OrderByDescending(a => a.Time.ToString())
.Take(take),
token);
return this.List<EntityAchievement, EntityAchievement>(query => query
.Where(a => a.ArchiveId == archiveId)
.Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
.OrderByDescending(a => a.Time.ToString())
.Take(take));
}
public void OverwriteAchievement(EntityAchievement achievement)
@@ -80,18 +74,8 @@ internal sealed partial class AchievementDbService : IAchievementDbService
return this.ListByArchiveId<EntityAchievement>(archiveId);
}
public ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId, CancellationToken token = default)
{
return this.ListByArchiveIdAsync<EntityAchievement>(archiveId, token);
}
public List<AchievementArchive> GetAchievementArchiveList()
{
return this.List<AchievementArchive>();
}
public ValueTask<List<AchievementArchive>> GetAchievementArchiveListAsync(CancellationToken token = default)
{
return this.ListAsync<AchievementArchive>(token);
}
}

View File

@@ -116,9 +116,7 @@ internal sealed partial class AchievementService : IAchievementService
public async ValueTask<UIAF> ExportToUIAFAsync(AchievementArchive archive)
{
await taskContext.SwitchToBackgroundAsync();
List<EntityAchievement> entities = await achievementDbService
.GetAchievementListByArchiveIdAsync(archive.InnerId)
.ConfigureAwait(false);
List<EntityAchievement> entities = achievementDbService.GetAchievementListByArchiveId(archive.InnerId);
List<UIAFItem> list = entities.SelectList(UIAFItem.From);
return new()

View File

@@ -22,17 +22,11 @@ internal sealed partial class AchievementStatisticsService : IAchievementStatist
await taskContext.SwitchToBackgroundAsync();
List<AchievementStatistics> results = [];
foreach (AchievementArchive archive in await achievementDbService.GetAchievementArchiveListAsync(token).ConfigureAwait(false))
foreach (AchievementArchive archive in achievementDbService.GetAchievementArchiveList())
{
int finishedCount = await achievementDbService
.GetFinishedAchievementCountByArchiveIdAsync(archive.InnerId, token)
.ConfigureAwait(false);
int finishedCount = achievementDbService.GetFinishedAchievementCountByArchiveId(archive.InnerId);
int totalCount = context.IdAchievementMap.Count;
List<EntityAchievement> achievements = await achievementDbService
.GetLatestFinishedAchievementListByArchiveIdAsync(archive.InnerId, AchievementCardTakeCount, token)
.ConfigureAwait(false);
List<EntityAchievement> achievements = achievementDbService.GetLatestFinishedAchievementListByArchiveId(archive.InnerId, AchievementCardTakeCount);
results.Add(new()
{

View File

@@ -14,17 +14,13 @@ internal interface IAchievementDbService : IAppDbService<Model.Entity.Achievemen
List<Model.Entity.AchievementArchive> GetAchievementArchiveList();
ValueTask<List<Model.Entity.AchievementArchive>> GetAchievementArchiveListAsync(CancellationToken token = default);
List<EntityAchievement> GetAchievementListByArchiveId(Guid archiveId);
ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId, CancellationToken token = default);
Dictionary<AchievementId, EntityAchievement> GetAchievementMapByArchiveId(Guid archiveId);
ValueTask<int> GetFinishedAchievementCountByArchiveIdAsync(Guid archiveId, CancellationToken token = default);
int GetFinishedAchievementCountByArchiveId(Guid archiveId);
ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take, CancellationToken token = default);
List<EntityAchievement> GetLatestFinishedAchievementListByArchiveId(Guid archiveId, int take);
void OverwriteAchievement(EntityAchievement achievement);

View File

@@ -27,12 +27,14 @@ namespace Snap.Hutao.Service.AvatarInfo;
[Injection(InjectAs.Singleton)]
internal sealed partial class AvatarInfoDbBulkOperation
{
private readonly IServiceProvider serviceProvider;
private readonly IAvatarInfoDbService avatarInfoDbService;
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
public async ValueTask<List<EntityAvatarInfo>> UpdateDbAvatarInfosByShowcaseAsync(string uid, IEnumerable<EnkaAvatarInfo> webInfos, CancellationToken token)
{
List<EntityAvatarInfo> dbInfos = await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid, token).ConfigureAwait(false);
await taskContext.SwitchToBackgroundAsync();
List<EntityAvatarInfo> dbInfos = avatarInfoDbService.GetAvatarInfoListByUid(uid);
EnsureItemsAvatarIdUnique(ref dbInfos, uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap);
using (IServiceScope scope = serviceProvider.CreateScope())
@@ -50,14 +52,15 @@ internal sealed partial class AvatarInfoDbBulkOperation
AddOrUpdateAvatarInfo(entity, uid, appDbContext, webInfo);
}
return await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid, token).ConfigureAwait(false);
return avatarInfoDbService.GetAvatarInfoListByUid(uid);
}
}
public async ValueTask<List<EntityAvatarInfo>> UpdateDbAvatarInfosByGameRecordCharacterAsync(UserAndUid userAndUid, CancellationToken token)
{
await taskContext.SwitchToBackgroundAsync();
string uid = userAndUid.Uid.Value;
List<EntityAvatarInfo> dbInfos = await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid, token).ConfigureAwait(false);
List<EntityAvatarInfo> dbInfos = avatarInfoDbService.GetAvatarInfoListByUid(uid);
EnsureItemsAvatarIdUnique(ref dbInfos, uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap);
using (IServiceScope scope = serviceProvider.CreateScope())
@@ -103,14 +106,14 @@ internal sealed partial class AvatarInfoDbBulkOperation
}
Return:
return await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid, token).ConfigureAwait(false);
return avatarInfoDbService.GetAvatarInfoListByUid(uid);
}
public async ValueTask<List<EntityAvatarInfo>> UpdateDbAvatarInfosByCalculateAvatarDetailAsync(UserAndUid userAndUid, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await taskContext.SwitchToBackgroundAsync();
string uid = userAndUid.Uid.Value;
List<EntityAvatarInfo> dbInfos = await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid, token).ConfigureAwait(false);
List<EntityAvatarInfo> dbInfos = avatarInfoDbService.GetAvatarInfoListByUid(uid);
EnsureItemsAvatarIdUnique(ref dbInfos, uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap);
using (IServiceScope scope = serviceProvider.CreateScope())
@@ -146,7 +149,7 @@ internal sealed partial class AvatarInfoDbBulkOperation
}
}
return await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid, token).ConfigureAwait(false);
return avatarInfoDbService.GetAvatarInfoListByUid(uid);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -19,18 +19,8 @@ internal sealed partial class AvatarInfoDbService : IAvatarInfoDbService
return this.List(i => i.Uid == uid);
}
public ValueTask<List<EntityAvatarInfo>> GetAvatarInfoListByUidAsync(string uid, CancellationToken token = default)
{
return this.ListAsync(i => i.Uid == uid, token);
}
public void RemoveAvatarInfoRangeByUid(string uid)
{
this.Delete(i => i.Uid == uid);
}
public async ValueTask RemoveAvatarInfoRangeByUidAsync(string uid, CancellationToken token = default)
{
await this.DeleteAsync(i => i.Uid == uid, token).ConfigureAwait(false);
}
}

View File

@@ -74,7 +74,7 @@ internal sealed partial class AvatarInfoService : IAvatarInfoService
default:
{
List<EntityAvatarInfo> list = await avatarInfoDbService.GetAvatarInfoListByUidAsync(userAndUid.Uid.Value, token).ConfigureAwait(false);
List<EntityAvatarInfo> list = avatarInfoDbService.GetAvatarInfoListByUid(userAndUid.Uid.Value);
Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false);
return new(RefreshResultKind.Ok, summary.Avatars.Count == 0 ? null : summary);
}

View File

@@ -11,8 +11,4 @@ internal interface IAvatarInfoDbService : IAppDbService<EntityAvatarInfo>
void RemoveAvatarInfoRangeByUid(string uid);
List<EntityAvatarInfo> GetAvatarInfoListByUid(string uid);
ValueTask<List<EntityAvatarInfo>> GetAvatarInfoListByUidAsync(string uid, CancellationToken token = default);
ValueTask RemoveAvatarInfoRangeByUidAsync(string uid, CancellationToken token = default);
}

View File

@@ -16,24 +16,24 @@ internal sealed partial class CultivationDbService : ICultivationDbService
public IServiceProvider ServiceProvider { get => serviceProvider; }
public ValueTask<List<CultivateEntry>> GetCultivateEntryListByProjectIdAsync(Guid projectId, CancellationToken token = default)
public List<CultivateEntry> GetCultivateEntryListByProjectId(Guid projectId)
{
return this.ListAsync<CultivateEntry>(e => e.ProjectId == projectId, token);
return this.List<CultivateEntry>(e => e.ProjectId == projectId);
}
public ValueTask<List<CultivateEntry>> GetCultivateEntryListIncludingLevelInformationByProjectIdAsync(Guid projectId, CancellationToken token = default)
public List<CultivateEntry> GetCultivateEntryListIncludingLevelInformationByProjectId(Guid projectId)
{
return this.ListAsync<CultivateEntry, CultivateEntry>(query => query.Where(e => e.ProjectId == projectId).Include(e => e.LevelInformation), token);
return this.List<CultivateEntry, CultivateEntry>(query => query.Where(e => e.ProjectId == projectId).Include(e => e.LevelInformation));
}
public ValueTask<List<CultivateItem>> GetCultivateItemListByEntryIdAsync(Guid entryId, CancellationToken token = default)
public List<CultivateItem> GetCultivateItemListByEntryId(Guid entryId)
{
return this.ListAsync<CultivateItem, CultivateItem>(query => query.Where(i => i.EntryId == entryId).OrderBy(i => i.ItemId), token);
return this.List<CultivateItem, CultivateItem>(query => query.Where(i => i.EntryId == entryId).OrderBy(i => i.ItemId));
}
public async ValueTask RemoveCultivateEntryByIdAsync(Guid entryId, CancellationToken token = default)
public void RemoveCultivateEntryById(Guid entryId)
{
await this.DeleteByInnerIdAsync<CultivateEntry>(entryId, token).ConfigureAwait(false);
this.DeleteByInnerId<CultivateEntry>(entryId);
}
public void UpdateCultivateItem(CultivateItem item)
@@ -41,39 +41,34 @@ internal sealed partial class CultivationDbService : ICultivationDbService
this.Update(item);
}
public async ValueTask UpdateCultivateItemAsync(CultivateItem item, CancellationToken token = default)
public CultivateEntry? GetCultivateEntryByProjectIdAndItemId(Guid projectId, uint itemId)
{
await this.UpdateAsync(item, token).ConfigureAwait(false);
return this.SingleOrDefault<CultivateEntry>(e => e.ProjectId == projectId && e.Id == itemId);
}
public async ValueTask<CultivateEntry?> GetCultivateEntryByProjectIdAndItemIdAsync(Guid projectId, uint itemId, CancellationToken token = default)
public void AddCultivateEntry(CultivateEntry entry)
{
return await this.SingleOrDefaultAsync<CultivateEntry>(e => e.ProjectId == projectId && e.Id == itemId, token).ConfigureAwait(false);
this.Add(entry);
}
public async ValueTask AddCultivateEntryAsync(CultivateEntry entry, CancellationToken token = default)
public void RemoveCultivateItemRangeByEntryId(Guid entryId)
{
await this.AddAsync(entry, token).ConfigureAwait(false);
this.Delete<CultivateItem>(i => i.EntryId == entryId);
}
public async ValueTask RemoveCultivateItemRangeByEntryIdAsync(Guid entryId, CancellationToken token = default)
public void AddCultivateItemRange(IEnumerable<CultivateItem> toAdd)
{
await this.DeleteAsync<CultivateItem>(i => i.EntryId == entryId, token).ConfigureAwait(false);
this.AddRange(toAdd);
}
public async ValueTask AddCultivateItemRangeAsync(IEnumerable<CultivateItem> toAdd, CancellationToken token = default)
public void AddCultivateProject(CultivateProject project)
{
await this.AddRangeAsync(toAdd, token).ConfigureAwait(false);
this.Add(project);
}
public async ValueTask AddCultivateProjectAsync(CultivateProject project, CancellationToken token = default)
public void RemoveCultivateProjectById(Guid projectId)
{
await this.AddAsync(project, token).ConfigureAwait(false);
}
public async ValueTask RemoveCultivateProjectByIdAsync(Guid projectId, CancellationToken token = default)
{
await this.DeleteByInnerIdAsync<CultivateProject>(projectId, token).ConfigureAwait(false);
this.DeleteByInnerId<CultivateProject>(projectId);
}
public ObservableCollection<CultivateProject> GetCultivateProjectCollection()
@@ -81,13 +76,13 @@ internal sealed partial class CultivationDbService : ICultivationDbService
return this.ObservableCollection<CultivateProject>();
}
public async ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId, CancellationToken token = default)
public void RemoveLevelInformationByEntryId(Guid entryId)
{
await this.DeleteAsync<CultivateEntryLevelInformation>(l => l.EntryId == entryId, token).ConfigureAwait(false);
this.Delete<CultivateEntryLevelInformation>(l => l.EntryId == entryId);
}
public async ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation, CancellationToken token = default)
public void AddLevelInformation(CultivateEntryLevelInformation levelInformation)
{
await this.AddAsync(levelInformation, token).ConfigureAwait(false);
this.Add(levelInformation);
}
}

View File

@@ -38,17 +38,14 @@ internal sealed partial class CultivationService : ICultivationService
public async ValueTask<ObservableCollection<CultivateEntryView>> GetCultivateEntriesAsync(CultivateProject cultivateProject, ICultivationMetadataContext context)
{
await taskContext.SwitchToBackgroundAsync();
List<CultivateEntry> entries = await cultivationDbService
.GetCultivateEntryListIncludingLevelInformationByProjectIdAsync(cultivateProject.InnerId)
.ConfigureAwait(false);
List<CultivateEntry> entries = cultivationDbService.GetCultivateEntryListIncludingLevelInformationByProjectId(cultivateProject.InnerId);
List<CultivateEntryView> resultEntries = new(entries.Count);
foreach (CultivateEntry entry in entries)
{
List<CultivateItemView> entryItems = [];
// Async operation here, thus we can't use the Span trick.
foreach (CultivateItem cultivateItem in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId).ConfigureAwait(false))
foreach (CultivateItem cultivateItem in cultivationDbService.GetCultivateItemListByEntryId(entry.InnerId))
{
entryItems.Add(new(cultivateItem, context.GetMaterial(cultivateItem.ItemId)));
}
@@ -77,9 +74,9 @@ internal sealed partial class CultivationService : ICultivationService
Guid projectId = cultivateProject.InnerId;
foreach (CultivateEntry entry in await cultivationDbService.GetCultivateEntryListByProjectIdAsync(projectId, token).ConfigureAwait(false))
foreach (CultivateEntry entry in cultivationDbService.GetCultivateEntryListByProjectId(projectId))
{
foreach (CultivateItem item in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId, token).ConfigureAwait(false))
foreach (CultivateItem item in cultivationDbService.GetCultivateItemListByEntryId(entry.InnerId))
{
if (item.IsFinished)
{
@@ -97,7 +94,7 @@ internal sealed partial class CultivationService : ICultivationService
}
}
foreach (InventoryItem inventoryItem in await inventoryDbService.GetInventoryItemListByProjectIdAsync(projectId, token).ConfigureAwait(false))
foreach (InventoryItem inventoryItem in inventoryDbService.GetInventoryItemListByProjectId(projectId))
{
if (resultItems.SingleOrDefault(i => i.Inner.Id == inventoryItem.ItemId) is { } existedItem)
{
@@ -111,7 +108,8 @@ internal sealed partial class CultivationService : ICultivationService
/// <inheritdoc/>
public async ValueTask RemoveCultivateEntryAsync(Guid entryId)
{
await cultivationDbService.RemoveCultivateEntryByIdAsync(entryId).ConfigureAwait(false);
await taskContext.SwitchToBackgroundAsync();
cultivationDbService.RemoveCultivateEntryById(entryId);
}
/// <inheritdoc/>
@@ -141,25 +139,23 @@ internal sealed partial class CultivationService : ICultivationService
return false;
}
CultivateEntry? entry = await cultivationDbService
.GetCultivateEntryByProjectIdAndItemIdAsync(Projects.CurrentItem.InnerId, itemId)
.ConfigureAwait(false);
CultivateEntry? entry = cultivationDbService.GetCultivateEntryByProjectIdAndItemId(Projects.CurrentItem.InnerId, itemId);
if (entry is null)
{
entry = CultivateEntry.From(Projects.CurrentItem.InnerId, type, itemId);
await cultivationDbService.AddCultivateEntryAsync(entry).ConfigureAwait(false);
cultivationDbService.AddCultivateEntry(entry);
}
Guid entryId = entry.InnerId;
await cultivationDbService.RemoveLevelInformationByEntryIdAsync(entryId).ConfigureAwait(false);
cultivationDbService.RemoveLevelInformationByEntryId(entryId);
CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entryId, type, levelInformation);
await cultivationDbService.AddLevelInformationAsync(entryLevelInformation).ConfigureAwait(false);
cultivationDbService.AddLevelInformation(entryLevelInformation);
await cultivationDbService.RemoveCultivateItemRangeByEntryIdAsync(entryId).ConfigureAwait(false);
cultivationDbService.RemoveCultivateItemRangeByEntryId(entryId);
IEnumerable<CultivateItem> toAdd = items.Select(item => CultivateItem.From(entryId, item));
await cultivationDbService.AddCultivateItemRangeAsync(toAdd).ConfigureAwait(false);
cultivationDbService.AddCultivateItemRange(toAdd);
return true;
}
@@ -199,6 +195,6 @@ internal sealed partial class CultivationService : ICultivationService
// Sync database
await taskContext.SwitchToBackgroundAsync();
await cultivationDbService.RemoveCultivateProjectByIdAsync(project.InnerId).ConfigureAwait(false);
cultivationDbService.RemoveCultivateProjectById(project.InnerId);
}
}

View File

@@ -12,33 +12,31 @@ internal interface ICultivationDbService : IAppDbService<CultivateEntryLevelInfo
IAppDbService<CultivateEntry>,
IAppDbService<CultivateItem>
{
ValueTask AddCultivateProjectAsync(CultivateProject project, CancellationToken token = default);
void AddCultivateProject(CultivateProject project);
ValueTask RemoveCultivateEntryByIdAsync(Guid entryId, CancellationToken token = default);
void RemoveCultivateEntryById(Guid entryId);
ValueTask RemoveCultivateItemRangeByEntryIdAsync(Guid entryId, CancellationToken token = default);
void RemoveCultivateItemRangeByEntryId(Guid entryId);
ValueTask RemoveCultivateProjectByIdAsync(Guid projectId, CancellationToken token = default);
void RemoveCultivateProjectById(Guid projectId);
ValueTask<CultivateEntry?> GetCultivateEntryByProjectIdAndItemIdAsync(Guid projectId, uint itemId, CancellationToken token = default);
CultivateEntry? GetCultivateEntryByProjectIdAndItemId(Guid projectId, uint itemId);
ValueTask<List<CultivateEntry>> GetCultivateEntryListByProjectIdAsync(Guid projectId, CancellationToken token = default);
List<CultivateEntry> GetCultivateEntryListByProjectId(Guid projectId);
ValueTask<List<CultivateItem>> GetCultivateItemListByEntryIdAsync(Guid entryId, CancellationToken token = default);
List<CultivateItem> GetCultivateItemListByEntryId(Guid entryId);
ObservableCollection<CultivateProject> GetCultivateProjectCollection();
ValueTask AddCultivateEntryAsync(CultivateEntry entry, CancellationToken token = default);
void AddCultivateEntry(CultivateEntry entry);
ValueTask AddCultivateItemRangeAsync(IEnumerable<CultivateItem> toAdd, CancellationToken token = default);
void AddCultivateItemRange(IEnumerable<CultivateItem> toAdd);
void UpdateCultivateItem(CultivateItem item);
ValueTask UpdateCultivateItemAsync(CultivateItem item, CancellationToken token = default);
void RemoveLevelInformationByEntryId(Guid entryId);
ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId, CancellationToken token = default);
void AddLevelInformation(CultivateEntryLevelInformation levelInformation);
ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation, CancellationToken token = default);
ValueTask<List<CultivateEntry>> GetCultivateEntryListIncludingLevelInformationByProjectIdAsync(Guid projectId, CancellationToken token = default);
List<CultivateEntry> GetCultivateEntryListIncludingLevelInformationByProjectId(Guid projectId);
}

View File

@@ -20,33 +20,23 @@ internal sealed partial class DailyNoteDbService : IDailyNoteDbService
return this.Query(query => query.Any(n => n.Uid == uid));
}
public ValueTask<bool> ContainsUidAsync(string uid, CancellationToken token = default)
public void AddDailyNoteEntry(DailyNoteEntry entry)
{
return this.QueryAsync(query => query.AnyAsync(n => n.Uid == uid));
this.Add(entry);
}
public async ValueTask AddDailyNoteEntryAsync(DailyNoteEntry entry, CancellationToken token = default)
public void DeleteDailyNoteEntryById(Guid entryId)
{
await this.AddAsync(entry, token).ConfigureAwait(false);
this.DeleteByInnerId(entryId);
}
public async ValueTask DeleteDailyNoteEntryByIdAsync(Guid entryId, CancellationToken token = default)
public void UpdateDailyNoteEntry(DailyNoteEntry entry)
{
await this.DeleteByInnerIdAsync(entryId, token).ConfigureAwait(false);
}
public async ValueTask UpdateDailyNoteEntryAsync(DailyNoteEntry entry, CancellationToken token = default)
{
await this.UpdateAsync(entry, token).ConfigureAwait(false);
this.Update(entry);
}
public List<DailyNoteEntry> GetDailyNoteEntryListIncludingUser()
{
return this.List(query => query.Include(n => n.User));
}
public ValueTask<List<DailyNoteEntry>> GetDailyNoteEntryListIncludingUserAsync(CancellationToken token = default)
{
return this.ListAsync(query => query.Include(n => n.User), token);
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
@@ -18,7 +19,7 @@ namespace Snap.Hutao.Service.DailyNote;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IDailyNoteService))]
internal sealed partial class DailyNoteService : IDailyNoteService
internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessage>
{
private readonly DailyNoteNotificationOperation dailyNoteNotificationOperation;
private readonly IServiceProvider serviceProvider;
@@ -28,19 +29,17 @@ internal sealed partial class DailyNoteService : IDailyNoteService
private ObservableCollection<DailyNoteEntry>? entries;
/// <inheritdoc/>
public void Receive(UserRemovedMessage message)
{
// Database items have been deleted by cascade deleting.
taskContext.BeginInvokeOnMainThread(() => entries?.RemoveWhere(n => n.UserId == message.RemovedUserId));
taskContext.BeginInvokeOnMainThread(() => entries?.RemoveWhere(n => n.UserId == message.RemovedUser.InnerId));
}
/// <inheritdoc/>
public async ValueTask AddDailyNoteAsync(UserAndUid userAndUid, CancellationToken token = default)
{
string roleUid = userAndUid.Uid.Value;
if (await dailyNoteDbService.ContainsUidAsync(roleUid, token).ConfigureAwait(false))
if (dailyNoteDbService.ContainsUid(roleUid))
{
return;
}
@@ -67,9 +66,9 @@ internal sealed partial class DailyNoteService : IDailyNoteService
newEntry.UpdateDailyNote(dailyNoteResponse.Data);
}
newEntry.UserGameRole = userService.GetUserGameRoleByUid(roleUid);
newEntry.UserGameRole = await userService.GetUserGameRoleByUidAsync(roleUid).ConfigureAwait(false);
newEntry.ArchonQuestView = DailyNoteArchonQuestView.Create(newEntry.DailyNote, context.Chapters);
await dailyNoteDbService.AddDailyNoteEntryAsync(newEntry, token).ConfigureAwait(false);
dailyNoteDbService.AddDailyNoteEntry(newEntry);
newEntry.User = userAndUid.User;
await taskContext.SwitchToMainThreadAsync();
@@ -81,20 +80,20 @@ internal sealed partial class DailyNoteService : IDailyNoteService
{
if (entries is null)
{
// IUserService.GetUserGameRoleByUid only usable after call IUserService.GetRoleCollectionAsync
await userService.GetRoleCollectionAsync().ConfigureAwait(false);
await RefreshDailyNotesCoreAsync(forceRefresh, token).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
DailyNoteMetadataContext context = await scope.GetRequiredService<IMetadataService>().GetContextAsync<DailyNoteMetadataContext>(token).ConfigureAwait(false);
List<DailyNoteEntry> entryList = await dailyNoteDbService.GetDailyNoteEntryListIncludingUserAsync(token).ConfigureAwait(false);
entryList.ForEach(entry =>
List<DailyNoteEntry> entryList = dailyNoteDbService.GetDailyNoteEntryListIncludingUser();
foreach (DailyNoteEntry entry in entryList)
{
entry.UserGameRole = userService.GetUserGameRoleByUid(entry.Uid);
entry.UserGameRole = await userService.GetUserGameRoleByUidAsync(entry.Uid).ConfigureAwait(false);
entry.ArchonQuestView = DailyNoteArchonQuestView.Create(entry.DailyNote, context.Chapters);
});
}
entries = entryList.ToObservableCollection();
}
}
@@ -116,22 +115,23 @@ internal sealed partial class DailyNoteService : IDailyNoteService
entries.Remove(entry);
await taskContext.SwitchToBackgroundAsync();
await dailyNoteDbService.DeleteDailyNoteEntryByIdAsync(entry.InnerId, token).ConfigureAwait(false);
dailyNoteDbService.DeleteDailyNoteEntryById(entry.InnerId);
}
public async ValueTask UpdateDailyNoteAsync(DailyNoteEntry entry, CancellationToken token = default)
{
await taskContext.SwitchToBackgroundAsync();
await dailyNoteDbService.UpdateDailyNoteEntryAsync(entry, token).ConfigureAwait(false);
dailyNoteDbService.UpdateDailyNoteEntry(entry);
}
private async ValueTask RefreshDailyNotesCoreAsync(bool forceRefresh, CancellationToken token = default)
{
await taskContext.SwitchToBackgroundAsync();
using (IServiceScope scope = serviceProvider.CreateScope())
{
DailyNoteWebhookOperation dailyNoteWebhookOperation = serviceProvider.GetRequiredService<DailyNoteWebhookOperation>();
foreach (DailyNoteEntry entry in await dailyNoteDbService.GetDailyNoteEntryListIncludingUserAsync(token).ConfigureAwait(false))
foreach (DailyNoteEntry entry in dailyNoteDbService.GetDailyNoteEntryListIncludingUser())
{
if (!forceRefresh && entry.DailyNote is not null)
{
@@ -163,7 +163,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService
{
// 发送通知必须早于数据库更新,否则会导致通知重复
await dailyNoteNotificationOperation.SendAsync(entry).ConfigureAwait(false);
await dailyNoteDbService.UpdateDailyNoteEntryAsync(entry, token).ConfigureAwait(false);
dailyNoteDbService.UpdateDailyNoteEntry(entry);
dailyNoteWebhookOperation.TryPostDailyNoteToWebhook(entry.Uid, dailyNote);
}
}

View File

@@ -8,17 +8,13 @@ namespace Snap.Hutao.Service.DailyNote;
internal interface IDailyNoteDbService : IAppDbService<DailyNoteEntry>
{
ValueTask AddDailyNoteEntryAsync(DailyNoteEntry entry, CancellationToken token = default);
void AddDailyNoteEntry(DailyNoteEntry entry);
bool ContainsUid(string uid);
ValueTask<bool> ContainsUidAsync(string uid, CancellationToken token = default);
ValueTask DeleteDailyNoteEntryByIdAsync(Guid entryId, CancellationToken token = default);
void DeleteDailyNoteEntryById(Guid entryId);
List<DailyNoteEntry> GetDailyNoteEntryListIncludingUser();
ValueTask<List<DailyNoteEntry>> GetDailyNoteEntryListIncludingUserAsync(CancellationToken token = default);
ValueTask UpdateDailyNoteEntryAsync(DailyNoteEntry entry, CancellationToken token = default);
void UpdateDailyNoteEntry(DailyNoteEntry entry);
}

View File

@@ -1,12 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using System.Collections.ObjectModel;
@@ -18,233 +16,49 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService
{
private readonly IServiceProvider serviceProvider;
public IServiceProvider ServiceProvider { get => serviceProvider; }
public ObservableCollection<GachaArchive> GetGachaArchiveCollection()
{
try
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
}
}
catch (SqliteException ex)
{
string message = SH.FormatServiceGachaLogArchiveCollectionUserdataCorruptedMessage(ex.Message);
throw HutaoException.Throw(message, ex);
}
return this.ObservableCollection<GachaArchive>();
}
public List<GachaItem> GetGachaItemListByArchiveId(Guid archiveId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IOrderedQueryable<GachaItem> result = appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.OrderBy(i => i.Id);
return [.. result];
}
return this.List<GachaItem, GachaItem>(query => query.Where(i => i.ArchiveId == archiveId).OrderBy(i => i.Id));
}
public async ValueTask<List<GachaItem>> GetGachaItemListByArchiveIdAsync(Guid archiveId)
public void RemoveGachaArchiveById(Guid archiveId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.OrderBy(i => i.Id)
.ToListAsync()
.ConfigureAwait(false);
}
}
public async ValueTask RemoveGachaArchiveByIdAsync(Guid archiveId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.GachaArchives
.Where(a => a.InnerId == archiveId)
.ExecuteDeleteAsync()
.ConfigureAwait(false);
}
}
public async ValueTask<long> GetNewestGachaItemIdByArchiveIdAndQueryTypeAsync(Guid archiveId, GachaType queryType, CancellationToken token)
{
GachaItem? item = null;
try
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = await appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.Where(i => i.QueryType == queryType)
.OrderByDescending(i => i.Id)
.FirstOrDefaultAsync(token)
.ConfigureAwait(false);
}
}
catch (SqliteException ex)
{
HutaoException.Throw(SH.ServiceGachaLogEndIdUserdataCorruptedMessage, ex);
}
return item?.Id ?? 0L;
this.DeleteByInnerId<GachaArchive>(archiveId);
}
public long GetNewestGachaItemIdByArchiveIdAndQueryType(Guid archiveId, GachaType queryType)
{
GachaItem? item = null;
try
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.Where(i => i.QueryType == queryType)
.OrderByDescending(i => i.Id)
.FirstOrDefault();
}
}
catch (SqliteException ex)
{
HutaoException.Throw(SH.ServiceGachaLogEndIdUserdataCorruptedMessage, ex);
}
return item?.Id ?? 0L;
}
public async ValueTask<long> GetNewestGachaItemIdByArchiveIdAndQueryTypeAsync(Guid archiveId, GachaType queryType)
{
GachaItem? item = null;
try
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = await appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.Where(i => i.QueryType == queryType)
.OrderByDescending(i => i.Id)
.FirstOrDefaultAsync()
.ConfigureAwait(false);
}
}
catch (SqliteException ex)
{
HutaoException.Throw(SH.ServiceGachaLogEndIdUserdataCorruptedMessage, ex);
}
GachaItem? item = this.Query<GachaItem, GachaItem?>(query => query
.Where(i => i.ArchiveId == archiveId && i.QueryType == queryType)
.OrderByDescending(i => i.Id)
.FirstOrDefault());
return item?.Id ?? 0L;
}
public long GetOldestGachaItemIdByArchiveId(Guid archiveId)
{
GachaItem? item = null;
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.OrderBy(i => i.Id)
.FirstOrDefault();
}
return item?.Id ?? long.MaxValue;
}
public async ValueTask<long> GetOldestGachaItemIdByArchiveIdAsync(Guid archiveId)
{
GachaItem? item = null;
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = await appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.OrderBy(i => i.Id)
.FirstOrDefaultAsync()
.ConfigureAwait(false);
}
GachaItem? item = this.Query<GachaItem, GachaItem?>(query => query
.Where(i => i.ArchiveId == archiveId)
.OrderBy(i => i.Id)
.FirstOrDefault());
return item?.Id ?? long.MaxValue;
}
public long GetOldestGachaItemIdByArchiveIdAndQueryType(Guid archiveId, GachaType queryType)
{
GachaItem? item = null;
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId && i.QueryType == queryType)
.OrderBy(i => i.Id)
.FirstOrDefault();
}
return item?.Id ?? long.MaxValue;
}
public async ValueTask<long> GetOldestGachaItemIdByArchiveIdAndQueryTypeAsync(Guid archiveId, GachaType queryType, CancellationToken token)
{
GachaItem? item = null;
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = await appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId && i.QueryType == queryType)
.OrderBy(i => i.Id)
.FirstOrDefaultAsync(token)
.ConfigureAwait(false);
}
GachaItem? item = this.Query<GachaItem, GachaItem?>(query => query
.Where(i => i.ArchiveId == archiveId && i.QueryType == queryType)
.OrderBy(i => i.Id)
.FirstOrDefault());
return item?.Id ?? long.MaxValue;
}
@@ -258,141 +72,41 @@ internal sealed partial class GachaLogDbService : IGachaLogDbService
}
}
public async ValueTask AddGachaArchiveAsync(GachaArchive archive)
[SuppressMessage("", "IDE0305")]
public List<Web.Hutao.GachaLog.GachaItem> GetHutaoGachaItemListByArchiveIdAndQueryTypeNewerThanEndId(Guid archiveId, GachaType queryType, long endId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.GachaArchives.AddAndSaveAsync(archive).ConfigureAwait(false);
}
return this.Query<GachaItem, List<Web.Hutao.GachaLog.GachaItem>>(query => query
.Where(i => i.ArchiveId == archiveId && i.QueryType == queryType)
.OrderByDescending(i => i.Id)
.Where(i => i.Id > endId)
.Select(i => new Web.Hutao.GachaLog.GachaItem()
{
GachaType = i.GachaType,
QueryType = i.QueryType,
ItemId = i.ItemId,
Time = i.Time,
Id = i.Id,
})
.ToList());
}
public List<Web.Hutao.GachaLog.GachaItem> GetHutaoGachaItemList(Guid archiveId, GachaType queryType, long endId)
public GachaArchive? GetGachaArchiveById(Guid archiveId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IQueryable<Web.Hutao.GachaLog.GachaItem> result = appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.Where(i => i.QueryType == queryType)
.OrderByDescending(i => i.Id)
.Where(i => i.Id > endId)
// Keep this to make SQL generates correctly
.Select(i => new Web.Hutao.GachaLog.GachaItem()
{
GachaType = i.GachaType,
QueryType = i.QueryType,
ItemId = i.ItemId,
Time = i.Time,
Id = i.Id,
});
return [.. result];
}
return this.SingleOrDefault<GachaArchive>(a => a.InnerId == archiveId);
}
public async ValueTask<List<Web.Hutao.GachaLog.GachaItem>> GetHutaoGachaItemListAsync(Guid archiveId, GachaType queryType, long endId)
public GachaArchive? GetGachaArchiveByUid(string uid)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.Where(i => i.QueryType == queryType)
.OrderByDescending(i => i.Id)
.Where(i => i.Id > endId)
// Keep this to make SQL generates correctly
.Select(i => new Web.Hutao.GachaLog.GachaItem()
{
GachaType = i.GachaType,
QueryType = i.QueryType,
ItemId = i.ItemId,
Time = i.Time,
Id = i.Id,
})
.ToListAsync()
.ConfigureAwait(false);
}
}
public async ValueTask<GachaArchive?> GetGachaArchiveByIdAsync(Guid archiveId, CancellationToken token)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.GachaArchives
.AsNoTracking()
.SingleOrDefaultAsync(a => a.InnerId == archiveId, token)
.ConfigureAwait(false);
}
}
public async ValueTask<GachaArchive?> GetGachaArchiveByUidAsync(string uid, CancellationToken token)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.GachaArchives
.AsNoTracking()
.SingleOrDefaultAsync(a => a.Uid == uid, token)
.ConfigureAwait(false);
}
}
public async ValueTask AddGachaItemsAsync(List<GachaItem> items)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.GachaItems.AddRangeAndSaveAsync(items).ConfigureAwait(false);
}
return this.SingleOrDefault<GachaArchive>(a => a.Uid == uid);
}
public void AddGachaItemRange(List<GachaItem> items)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.GachaItems.AddRangeAndSave(items);
}
this.AddRange(items);
}
public async ValueTask AddGachaItemRangeAsync(List<GachaItem> items)
public void RemoveGachaItemRangeByArchiveIdAndQueryTypeNewerThanEndId(Guid archiveId, GachaType queryType, long endId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.GachaItems.AddRangeAndSaveAsync(items).ConfigureAwait(false);
}
}
public void RemoveNewerGachaItemRangeByArchiveIdQueryTypeAndEndId(Guid archiveId, GachaType queryType, long endId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId && i.QueryType == queryType)
.Where(i => i.Id >= endId)
.ExecuteDelete();
}
}
public async ValueTask RemoveNewerGachaItemRangeByArchiveIdQueryTypeAndEndIdAsync(Guid archiveId, GachaType queryType, long endId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.GachaItems
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId && i.QueryType == queryType)
.Where(i => i.Id >= endId)
.ExecuteDeleteAsync()
.ConfigureAwait(false);
}
this.Delete<GachaItem>(i => i.ArchiveId == archiveId && i.QueryType == queryType && i.Id >= endId);
}
}

View File

@@ -87,7 +87,7 @@ internal struct GachaLogFetchContext
// 全量刷新
if (!isLazy)
{
gachaLogDbService.RemoveNewerGachaItemRangeByArchiveIdQueryTypeAndEndId(TargetArchive.InnerId, TypedQueryOptions.Type, TypedQueryOptions.EndId);
gachaLogDbService.RemoveGachaItemRangeByArchiveIdAndQueryTypeNewerThanEndId(TargetArchive.InnerId, TypedQueryOptions.Type, TypedQueryOptions.EndId);
}
gachaLogDbService.AddGachaItemRange(ItemsToAdd);

View File

@@ -39,10 +39,7 @@ internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudSer
List<Web.Hutao.GachaLog.GachaItem> items = [];
foreach ((GachaType type, long endId) in endIds)
{
List<Web.Hutao.GachaLog.GachaItem> part = await gachaLogDbService
.GetHutaoGachaItemListAsync(gachaArchive.InnerId, type, endId)
.ConfigureAwait(false);
items.AddRange(part);
items.AddRange(gachaLogDbService.GetHutaoGachaItemListByArchiveIdAndQueryTypeNewerThanEndId(gachaArchive.InnerId, type, endId));
}
return await homaGachaLogClient.UploadGachaItemsAsync(uid, items, token).ConfigureAwait(false);
@@ -54,11 +51,8 @@ internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudSer
/// <inheritdoc/>
public async ValueTask<ValueResult<bool, Guid>> RetrieveGachaArchiveIdAsync(string uid, CancellationToken token = default)
{
GachaArchive? archive = await gachaLogDbService
.GetGachaArchiveByUidAsync(uid, token)
.ConfigureAwait(false);
EndIds endIds = await CreateEndIdsAsync(archive, token).ConfigureAwait(false);
GachaArchive? archive = gachaLogDbService.GetGachaArchiveByUid(uid);
EndIds endIds = CreateEndIds(archive);
Response<List<Web.Hutao.GachaLog.GachaItem>> resp = await homaGachaLogClient
.RetrieveGachaItemsAsync(uid, endIds, token)
.ConfigureAwait(false);
@@ -71,12 +65,12 @@ internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudSer
if (archive is null)
{
archive = GachaArchive.From(uid);
await gachaLogDbService.AddGachaArchiveAsync(archive).ConfigureAwait(false);
gachaLogDbService.AddGachaArchive(archive);
}
Guid archiveId = archive.InnerId;
List<Model.Entity.GachaItem> gachaItems = resp.Data.SelectList(i => Model.Entity.GachaItem.From(archiveId, i));
await gachaLogDbService.AddGachaItemsAsync(gachaItems).ConfigureAwait(false);
gachaLogDbService.AddGachaItemRange(gachaItems);
return new(true, archive.InnerId);
}
@@ -122,16 +116,14 @@ internal sealed partial class GachaLogHutaoCloudService : IGachaLogHutaoCloudSer
return resp.IsOk() ? resp.Data : default;
}
private async ValueTask<EndIds> CreateEndIdsAsync(GachaArchive? archive, CancellationToken token)
private EndIds CreateEndIds(GachaArchive? archive)
{
EndIds endIds = new();
foreach (GachaType type in GachaLog.QueryTypes)
{
if (archive is not null)
{
endIds[type] = await gachaLogDbService
.GetOldestGachaItemIdByArchiveIdAndQueryTypeAsync(archive.InnerId, type, token)
.ConfigureAwait(false);
endIds[type] = gachaLogDbService.GetOldestGachaItemIdByArchiveIdAndQueryType(archive.InnerId, type);
}
}

View File

@@ -64,7 +64,7 @@ internal sealed partial class GachaLogService : IGachaLogService
{
using (ValueStopwatch.MeasureExecution(logger))
{
List<GachaItem> items = await gachaLogDbService.GetGachaItemListByArchiveIdAsync(archive.InnerId).ConfigureAwait(false);
List<GachaItem> items = gachaLogDbService.GetGachaItemListByArchiveId(archive.InnerId);
return await gachaStatisticsFactory.CreateAsync(items, context).ConfigureAwait(false);
}
}
@@ -77,7 +77,7 @@ internal sealed partial class GachaLogService : IGachaLogService
List<GachaStatisticsSlim> statistics = [];
foreach (GachaArchive archive in Archives)
{
List<GachaItem> items = await gachaLogDbService.GetGachaItemListByArchiveIdAsync(archive.InnerId).ConfigureAwait(false);
List<GachaItem> items = gachaLogDbService.GetGachaItemListByArchiveId(archive.InnerId);
GachaStatisticsSlim slim = await gachaStatisticsSlimFactory.CreateAsync(context, items, archive.Uid).ConfigureAwait(false);
statistics.Add(slim);
}
@@ -121,7 +121,7 @@ internal sealed partial class GachaLogService : IGachaLogService
// Sync database
await taskContext.SwitchToBackgroundAsync();
await gachaLogDbService.RemoveGachaArchiveByIdAsync(archive.InnerId).ConfigureAwait(false);
gachaLogDbService.RemoveGachaArchiveById(archive.InnerId);
// Sync cache
await taskContext.SwitchToMainThreadAsync();
@@ -138,7 +138,7 @@ internal sealed partial class GachaLogService : IGachaLogService
}
else
{
GachaArchive? newArchive = await gachaLogDbService.GetGachaArchiveByIdAsync(archiveId, token).ConfigureAwait(false);
GachaArchive? newArchive = gachaLogDbService.GetGachaArchiveById(archiveId);
ArgumentNullException.ThrowIfNull(newArchive);
await taskContext.SwitchToMainThreadAsync();

View File

@@ -2,54 +2,35 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.GachaLog;
internal interface IGachaLogDbService
internal interface IGachaLogDbService : IAppDbService<GachaArchive>, IAppDbService<GachaItem>
{
void AddGachaArchive(GachaArchive archive);
ValueTask AddGachaArchiveAsync(GachaArchive archive);
void AddGachaItemRange(List<GachaItem> items);
ValueTask AddGachaItemsAsync(List<GachaItem> items);
void RemoveGachaArchiveById(Guid archiveId);
ValueTask RemoveGachaArchiveByIdAsync(Guid archiveId);
void RemoveGachaItemRangeByArchiveIdAndQueryTypeNewerThanEndId(Guid archiveId, GachaType queryType, long endId);
void RemoveNewerGachaItemRangeByArchiveIdQueryTypeAndEndId(Guid archiveId, GachaType queryType, long endId);
GachaArchive? GetGachaArchiveById(Guid archiveId);
ValueTask<GachaArchive?> GetGachaArchiveByIdAsync(Guid archiveId, CancellationToken token);
ValueTask<GachaArchive?> GetGachaArchiveByUidAsync(string uid, CancellationToken token);
GachaArchive? GetGachaArchiveByUid(string uid);
ObservableCollection<GachaArchive> GetGachaArchiveCollection();
List<GachaItem> GetGachaItemListByArchiveId(Guid archiveId);
ValueTask<List<GachaItem>> GetGachaItemListByArchiveIdAsync(Guid archiveId);
List<Web.Hutao.GachaLog.GachaItem> GetHutaoGachaItemList(Guid archiveId, GachaType queryType, long endId);
List<Web.Hutao.GachaLog.GachaItem> GetHutaoGachaItemListByArchiveIdAndQueryTypeNewerThanEndId(Guid archiveId, GachaType queryType, long endId);
long GetNewestGachaItemIdByArchiveIdAndQueryType(Guid archiveId, GachaType queryType);
ValueTask<long> GetNewestGachaItemIdByArchiveIdAndQueryTypeAsync(Guid archiveId, GachaType queryType, CancellationToken token);
long GetOldestGachaItemIdByArchiveId(Guid archiveId);
long GetOldestGachaItemIdByArchiveIdAndQueryType(Guid archiveId, GachaType queryType);
ValueTask<long> GetOldestGachaItemIdByArchiveIdAndQueryTypeAsync(Guid archiveId, GachaType queryType, CancellationToken token);
ValueTask<long> GetNewestGachaItemIdByArchiveIdAndQueryTypeAsync(Guid archiveId, GachaType queryType);
ValueTask<long> GetOldestGachaItemIdByArchiveIdAsync(Guid archiveId);
ValueTask<List<Web.Hutao.GachaLog.GachaItem>> GetHutaoGachaItemListAsync(Guid archiveId, GachaType queryType, long endId);
ValueTask AddGachaItemRangeAsync(List<GachaItem> items);
ValueTask RemoveNewerGachaItemRangeByArchiveIdQueryTypeAndEndIdAsync(Guid archiveId, GachaType queryType, long endId);
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.Service.User;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Request;
using Snap.Hutao.Web.Response;
@@ -25,7 +24,7 @@ internal sealed partial class GachaLogQuerySTokenProvider : IGachaLogQueryProvid
/// <inheritdoc/>
public async ValueTask<ValueResult<bool, GachaLogQuery>> GetQueryAsync()
{
if (!UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
{
return new(false, GachaLogQuery.Invalid(SH.MustSelectUserAndUid));
}

View File

@@ -23,9 +23,7 @@ internal sealed partial class UIGFExportService : IUIGFExportService
public async ValueTask<UIGF> ExportAsync(GachaLogServiceMetadataContext context, GachaArchive archive)
{
await taskContext.SwitchToBackgroundAsync();
List<GachaItem> entities = await gachaLogDbService
.GetGachaItemListByArchiveIdAsync(archive.InnerId)
.ConfigureAwait(false);
List<GachaItem> entities = gachaLogDbService.GetGachaItemListByArchiveId(archive.InnerId);
List<UIGFItem> list = entities.SelectList(i => UIGFItem.From(i, context.GetNameQualityByItemId(i.ItemId)));
UIGF uigf = new()

View File

@@ -82,7 +82,7 @@ internal sealed partial class UIGFImportService : IUIGFImportService
fullItems.AddRange(currentTypedList);
}
await gachaLogDbService.AddGachaItemsAsync(fullItems).ConfigureAwait(false);
gachaLogDbService.AddGachaItemRange(fullItems);
archives.MoveCurrentTo(archive);
}

View File

@@ -53,7 +53,7 @@ internal sealed partial class GameAccountService : IGameAccountService
// sync database
await taskContext.SwitchToBackgroundAsync();
await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false);
gameDbService.AddGameAccount(account);
// sync cache
await taskContext.SwitchToMainThreadAsync();
@@ -106,7 +106,7 @@ internal sealed partial class GameAccountService : IGameAccountService
// sync database
await taskContext.SwitchToBackgroundAsync();
await gameDbService.UpdateGameAccountAsync(gameAccount).ConfigureAwait(false);
gameDbService.UpdateGameAccount(gameAccount);
}
}
@@ -118,7 +118,7 @@ internal sealed partial class GameAccountService : IGameAccountService
gameAccounts.Remove(gameAccount);
await taskContext.SwitchToBackgroundAsync();
await gameDbService.RemoveGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false);
gameDbService.RemoveGameAccountById(gameAccount.InnerId);
}
private static GameAccount? SingleGameAccountOrDefault(ObservableCollection<GameAccount> gameAccounts, string registrySdk)

View File

@@ -1,10 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service.Game;
@@ -14,48 +13,25 @@ internal sealed partial class GameDbService : IGameDbService
{
private readonly IServiceProvider serviceProvider;
public IServiceProvider ServiceProvider { get => serviceProvider; }
public ObservableReorderableDbCollection<GameAccount> GetGameAccountCollection()
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.GameAccounts.AsNoTracking().ToObservableReorderableDbCollection(serviceProvider);
}
return this.Query(query => query.ToObservableReorderableDbCollection(serviceProvider));
}
public async ValueTask AddGameAccountAsync(GameAccount gameAccount)
public void AddGameAccount(GameAccount gameAccount)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.GameAccounts.AddAndSaveAsync(gameAccount).ConfigureAwait(false);
}
this.Add(gameAccount);
}
public void UpdateGameAccount(GameAccount gameAccount)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.GameAccounts.UpdateAndSave(gameAccount);
}
this.Update(gameAccount);
}
public async ValueTask UpdateGameAccountAsync(GameAccount gameAccount)
public void RemoveGameAccountById(Guid id)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.GameAccounts.UpdateAndSaveAsync(gameAccount).ConfigureAwait(false);
}
}
public async ValueTask RemoveGameAccountByIdAsync(Guid id)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.GameAccounts.Where(a => a.InnerId == id).ExecuteDeleteAsync().ConfigureAwait(false);
}
this.DeleteByInnerId(id);
}
}

View File

@@ -3,18 +3,17 @@
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service.Game;
internal interface IGameDbService
internal interface IGameDbService : IAppDbService<GameAccount>
{
ValueTask AddGameAccountAsync(GameAccount gameAccount);
void AddGameAccount(GameAccount gameAccount);
ValueTask RemoveGameAccountByIdAsync(Guid id);
void RemoveGameAccountById(Guid id);
ObservableReorderableDbCollection<GameAccount> GetGameAccountCollection();
void UpdateGameAccount(GameAccount gameAccount);
ValueTask UpdateGameAccountAsync(GameAccount gameAccount);
}

View File

@@ -12,22 +12,24 @@ namespace Snap.Hutao.Service.Hutao;
[Injection(InjectAs.Singleton, typeof(IObjectCacheDbService))]
internal sealed partial class ObjectCacheDbService : IObjectCacheDbService
{
private readonly IServiceProvider serviceProvider;
private readonly JsonSerializerOptions jsonSerializerOptions;
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
public async ValueTask AddObjectCacheAsync<T>(string key, TimeSpan expire, T data)
where T : class
{
await taskContext.SwitchToBackgroundAsync();
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.ObjectCache.AddAndSaveAsync(new()
appDbContext.ObjectCache.AddAndSave(new()
{
Key = key,
ExpireTime = DateTimeOffset.UtcNow.Add(expire),
Value = JsonSerializer.Serialize(data, jsonSerializerOptions),
}).ConfigureAwait(false);
});
}
}
@@ -36,6 +38,7 @@ internal sealed partial class ObjectCacheDbService : IObjectCacheDbService
{
try
{
await taskContext.SwitchToBackgroundAsync();
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
@@ -49,7 +52,7 @@ internal sealed partial class ObjectCacheDbService : IObjectCacheDbService
return value;
}
await appDbContext.ObjectCache.RemoveAndSaveAsync(entry).ConfigureAwait(false);
appDbContext.ObjectCache.RemoveAndSave(entry);
}
}
}

View File

@@ -8,15 +8,11 @@ namespace Snap.Hutao.Service.Inventory;
internal interface IInventoryDbService : IAppDbService<InventoryItem>
{
ValueTask AddInventoryItemRangeByProjectIdAsync(List<InventoryItem> items, CancellationToken token = default);
void AddInventoryItemRangeByProjectId(List<InventoryItem> items);
ValueTask RemoveInventoryItemRangeByProjectIdAsync(Guid projectId, CancellationToken token = default);
void RemoveInventoryItemRangeByProjectId(Guid projectId);
void UpdateInventoryItem(InventoryItem item);
ValueTask UpdateInventoryItemAsync(InventoryItem item, CancellationToken token = default);
List<InventoryItem> GetInventoryItemListByProjectId(Guid projectId);
ValueTask<List<InventoryItem>> GetInventoryItemListByProjectIdAsync(Guid projectId, CancellationToken token = default);
}

View File

@@ -14,14 +14,14 @@ internal sealed partial class InventoryDbService : IInventoryDbService
public IServiceProvider ServiceProvider { get => serviceProvider; }
public async ValueTask RemoveInventoryItemRangeByProjectIdAsync(Guid projectId, CancellationToken token = default)
public void RemoveInventoryItemRangeByProjectId(Guid projectId)
{
await this.DeleteAsync(i => i.ProjectId == projectId, token).ConfigureAwait(false);
this.Delete(i => i.ProjectId == projectId);
}
public async ValueTask AddInventoryItemRangeByProjectIdAsync(List<InventoryItem> items, CancellationToken token = default)
public void AddInventoryItemRangeByProjectId(List<InventoryItem> items)
{
await this.AddRangeAsync(items, token).ConfigureAwait(false);
this.AddRange(items);
}
public void UpdateInventoryItem(InventoryItem item)
@@ -29,18 +29,8 @@ internal sealed partial class InventoryDbService : IInventoryDbService
this.Update(item);
}
public async ValueTask UpdateInventoryItemAsync(InventoryItem item, CancellationToken token = default)
{
await this.UpdateAsync(item, token).ConfigureAwait(false);
}
public List<InventoryItem> GetInventoryItemListByProjectId(Guid projectId)
{
return this.List(i => i.ProjectId == projectId);
}
public ValueTask<List<InventoryItem>> GetInventoryItemListByProjectIdAsync(Guid projectId, CancellationToken token = default)
{
return this.ListAsync(i => i.ProjectId == projectId, token);
}
}
}

View File

@@ -8,7 +8,6 @@ using Snap.Hutao.Service.Metadata.ContextAbstraction;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.ViewModel.Cultivation;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
using Snap.Hutao.Web.Response;
@@ -54,7 +53,7 @@ internal sealed partial class InventoryService : IInventoryService
BatchConsumption? batchConsumption = default;
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
if (!UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
{
infoBarService.Warning(SH.MustSelectUserAndUid);
return;
@@ -76,8 +75,8 @@ internal sealed partial class InventoryService : IInventoryService
if (batchConsumption is { OverallConsume: { } items })
{
await inventoryDbService.RemoveInventoryItemRangeByProjectIdAsync(project.InnerId).ConfigureAwait(false);
await inventoryDbService.AddInventoryItemRangeByProjectIdAsync(items.SelectList(item => InventoryItem.From(project.InnerId, item.Id, (uint)((int)item.Num - item.LackNum)))).ConfigureAwait(false);
inventoryDbService.RemoveInventoryItemRangeByProjectId(project.InnerId);
inventoryDbService.AddInventoryItemRangeByProjectId(items.SelectList(item => InventoryItem.From(project.InnerId, item.Id, (uint)((int)item.Num - item.LackNum))));
}
}
}

View File

@@ -2,14 +2,15 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service.SpiralAbyss;
internal interface ISpiralAbyssRecordDbService
internal interface ISpiralAbyssRecordDbService : IAppDbService<SpiralAbyssEntry>
{
ValueTask AddSpiralAbyssEntryAsync(SpiralAbyssEntry entry);
void AddSpiralAbyssEntry(SpiralAbyssEntry entry);
ValueTask<Dictionary<uint, SpiralAbyssEntry>> GetSpiralAbyssEntryListByUidAsync(string uid);
Dictionary<uint, SpiralAbyssEntry> GetSpiralAbyssEntryListByUid(string uid);
ValueTask UpdateSpiralAbyssEntryAsync(SpiralAbyssEntry entry);
void UpdateSpiralAbyssEntry(SpiralAbyssEntry entry);
}

View File

@@ -1,10 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service.SpiralAbyss;
@@ -14,36 +12,23 @@ internal sealed partial class SpiralAbyssRecordDbService : ISpiralAbyssRecordDbS
{
private readonly IServiceProvider serviceProvider;
public async ValueTask<Dictionary<uint, SpiralAbyssEntry>> GetSpiralAbyssEntryListByUidAsync(string uid)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
List<SpiralAbyssEntry> entries = await appDbContext.SpiralAbysses
.Where(s => s.Uid == uid)
.OrderByDescending(s => s.ScheduleId)
.ToListAsync()
.ConfigureAwait(false);
public IServiceProvider ServiceProvider { get => serviceProvider; }
return entries.ToDictionary(e => e.ScheduleId);
}
public Dictionary<uint, SpiralAbyssEntry> GetSpiralAbyssEntryListByUid(string uid)
{
return this.Query(query => query
.Where(s => s.Uid == uid)
.OrderByDescending(s => s.ScheduleId)
.ToDictionary(e => e.ScheduleId));
}
public async ValueTask UpdateSpiralAbyssEntryAsync(SpiralAbyssEntry entry)
public void UpdateSpiralAbyssEntry(SpiralAbyssEntry entry)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.SpiralAbysses.UpdateAndSaveAsync(entry).ConfigureAwait(false);
}
this.Update(entry);
}
public async ValueTask AddSpiralAbyssEntryAsync(SpiralAbyssEntry entry)
public void AddSpiralAbyssEntry(SpiralAbyssEntry entry)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.SpiralAbysses.AddAndSaveAsync(entry).ConfigureAwait(false);
}
this.Add(entry);
}
}

View File

@@ -59,9 +59,8 @@ internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordServi
uid = userAndUid.Uid.Value;
if (spiralAbysses is null)
{
Dictionary<uint, SpiralAbyssEntry> entryMap = await spiralAbyssRecordDbService
.GetSpiralAbyssEntryListByUidAsync(userAndUid.Uid.Value)
.ConfigureAwait(false);
await taskContext.SwitchToBackgroundAsync();
Dictionary<uint, SpiralAbyssEntry> entryMap = spiralAbyssRecordDbService.GetSpiralAbyssEntryListByUid(userAndUid.Uid.Value);
ArgumentNullException.ThrowIfNull(metadataContext);
spiralAbysses = metadataContext.IdScheduleMap.Values
@@ -127,13 +126,13 @@ internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordServi
if (view.Entity is not null)
{
view.Entity.SpiralAbyss = webSpiralAbyss;
await spiralAbyssRecordDbService.UpdateSpiralAbyssEntryAsync(view.Entity).ConfigureAwait(false);
spiralAbyssRecordDbService.UpdateSpiralAbyssEntry(view.Entity);
targetEntry = view.Entity;
}
else
{
SpiralAbyssEntry newEntry = SpiralAbyssEntry.From(userAndUid.Uid.Value, webSpiralAbyss);
await spiralAbyssRecordDbService.AddSpiralAbyssEntryAsync(newEntry).ConfigureAwait(false);
spiralAbyssRecordDbService.AddSpiralAbyssEntry(newEntry);
targetEntry = newEntry;
}

View File

@@ -8,9 +8,9 @@ namespace Snap.Hutao.Service.User;
internal interface IUidProfilePictureDbService : IAppDbService<UidProfilePicture>
{
ValueTask<UidProfilePicture?> SingleUidProfilePictureOrDefaultByUidAsync(string uid, CancellationToken token = default);
UidProfilePicture? SingleUidProfilePictureOrDefaultByUid(string uid);
ValueTask UpdateUidProfilePictureAsync(UidProfilePicture profilePicture, CancellationToken token = default);
void UpdateUidProfilePicture(UidProfilePicture profilePicture);
ValueTask DeleteUidProfilePictureByUidAsync(string uid, CancellationToken token = default);
}
void DeleteUidProfilePictureByUid(string uid);
}

View File

@@ -2,9 +2,6 @@
// 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;
@@ -12,15 +9,9 @@ namespace Snap.Hutao.Service.User;
internal interface IUserCollectionService
{
ValueTask<ObservableCollection<UserAndUid>> GetUserAndUidCollectionAsync();
ValueTask<AdvancedDbCollectionView<BindingUser, EntityUser>> GetUserCollectionAsync();
UserGameRole? GetUserGameRoleByUid(string uid);
ValueTask<AdvancedDbCollectionView<BindingUser, EntityUser>> GetUsersAsync();
ValueTask RemoveUserAsync(BindingUser user);
ValueTask<ValueResult<UserOptionResult, string>> TryCreateAndAddUserFromInputCookieAsync(InputCookie inputCookie);
bool TryGetUserByMid(string mid, [NotNullWhen(true)] out BindingUser? user);
}

View File

@@ -1,17 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service.User;
internal interface IUserDbService
internal interface IUserDbService : IAppDbService<Model.Entity.User>
{
ValueTask DeleteUserByIdAsync(Guid id);
void DeleteUserById(Guid id);
ValueTask RemoveUsersAsync();
void RemoveAllUsers();
ValueTask<List<Model.Entity.User>> GetUserListAsync();
List<Model.Entity.User> GetUserList();
ValueTask UpdateUserAsync(Model.Entity.User user);
void UpdateUser(Model.Entity.User user);
ValueTask ClearUserSelectionAsync();
void ClearUserSelection();
}

View File

@@ -10,7 +10,7 @@ namespace Snap.Hutao.Service.User;
internal interface IUserService
{
ValueTask<AdvancedDbCollectionView<BindingUser, EntityUser>> GetUserCollectionAsync();
ValueTask<AdvancedDbCollectionView<BindingUser, EntityUser>> GetUsersAsync();
ValueTask<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(InputCookie inputCookie);

View File

@@ -24,7 +24,7 @@ internal sealed partial class ProfilePictureService : IProfilePictureService
{
foreach (UserGameRole userGameRole in user.UserGameRoles)
{
if (await uidProfilePictureDbService.SingleUidProfilePictureOrDefaultByUidAsync(userGameRole.GameUid, token).ConfigureAwait(false) is { } profilePicture)
if (uidProfilePictureDbService.SingleUidProfilePictureOrDefaultByUid(userGameRole.GameUid) is { } profilePicture)
{
if (await TryUpdateUserGameRoleAsync(userGameRole, profilePicture, token).ConfigureAwait(false))
{
@@ -53,8 +53,8 @@ internal sealed partial class ProfilePictureService : IProfilePictureService
{
UidProfilePicture profilePicture = UidProfilePicture.From(userGameRole, playerInfo.ProfilePicture);
await uidProfilePictureDbService.DeleteUidProfilePictureByUidAsync(userGameRole.GameUid, token).ConfigureAwait(false);
await uidProfilePictureDbService.UpdateUidProfilePictureAsync(profilePicture, token).ConfigureAwait(false);
uidProfilePictureDbService.DeleteUidProfilePictureByUid(userGameRole.GameUid);
uidProfilePictureDbService.UpdateUidProfilePicture(profilePicture);
await TryUpdateUserGameRoleAsync(userGameRole, profilePicture, token).ConfigureAwait(false);
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
@@ -15,18 +14,18 @@ internal sealed partial class UidProfilePictureDbService : IUidProfilePictureDbS
public IServiceProvider ServiceProvider { get => serviceProvider; }
public ValueTask<UidProfilePicture?> SingleUidProfilePictureOrDefaultByUidAsync(string uid, CancellationToken token = default)
public UidProfilePicture? SingleUidProfilePictureOrDefaultByUid(string uid)
{
return this.QueryAsync(query => query.SingleOrDefaultAsync(n => n.Uid == uid));
return this.Query(query => query.SingleOrDefault(n => n.Uid == uid));
}
public async ValueTask UpdateUidProfilePictureAsync(UidProfilePicture profilePicture, CancellationToken token = default)
public void UpdateUidProfilePicture(UidProfilePicture profilePicture)
{
await this.UpdateAsync(profilePicture, token).ConfigureAwait(false);
this.Update(profilePicture);
}
public async ValueTask DeleteUidProfilePictureByUidAsync(string uid, CancellationToken token = default)
public void DeleteUidProfilePictureByUid(string uid)
{
await this.DeleteAsync(profilePicture => profilePicture.Uid == uid, token).ConfigureAwait(false);
this.Delete(profilePicture => profilePicture.Uid == uid);
}
}

View File

@@ -5,7 +5,6 @@ using CommunityToolkit.Mvvm.Messaging;
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;
@@ -25,7 +24,7 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
private AdvancedDbCollectionView<BindingUser, EntityUser>? users;
public async ValueTask<AdvancedDbCollectionView<BindingUser, EntityUser>> GetUserCollectionAsync()
public async ValueTask<AdvancedDbCollectionView<BindingUser, EntityUser>> GetUsersAsync()
{
// Force run in background thread, otherwise will cause reentrance
await taskContext.SwitchToBackgroundAsync();
@@ -33,88 +32,40 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
{
if (users is null)
{
List<EntityUser> entities = await userDbService.GetUserListAsync().ConfigureAwait(false);
List<EntityUser> entities = userDbService.GetUserList();
List<BindingUser> users = await entities.SelectListAsync(userInitializationService.ResumeUserAsync).ConfigureAwait(false);
foreach (BindingUser user in users)
{
if (user.NeedDbUpdateAfterResume)
{
await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false);
userDbService.UpdateUser(user.Entity);
user.NeedDbUpdateAfterResume = false;
}
}
await taskContext.SwitchToMainThreadAsync();
this.users = new(users.ToObservableReorderableDbCollection<BindingUser, EntityUser>(serviceProvider), serviceProvider);
this.users.CurrentChanged += OnCurrentUserChanged;
}
}
return users;
}
public async ValueTask<ObservableCollection<UserAndUid>> GetUserAndUidCollectionAsync()
{
if (userAndUidCollection is null)
{
await taskContext.SwitchToBackgroundAsync();
ObservableCollection<BindingUser> users = await GetUserCollectionAsync().ConfigureAwait(false);
List<UserAndUid> roles = [];
uidUserGameRoleMap = [];
foreach (BindingUser user in users)
{
foreach (UserGameRole role in user.UserGameRoles)
{
roles.Add(UserAndUid.From(user.Entity, role));
uidUserGameRoleMap[role.GameUid] = role;
}
}
userAndUidCollection = roles.ToObservableCollection();
}
return userAndUidCollection;
}
public async ValueTask RemoveUserAsync(BindingUser user)
{
// Sync cache
await taskContext.SwitchToMainThreadAsync();
ArgumentNullException.ThrowIfNull(users);
users.Remove(user);
userAndUidCollection?.RemoveWhere(r => r.User.Mid == user.Entity.Mid);
if (user.Entity.Mid is not null)
{
midUserMap?.Remove(user.Entity.Mid);
}
foreach (UserGameRole role in user.UserGameRoles)
{
uidUserGameRoleMap?.Remove(role.GameUid);
}
// Sync database
await taskContext.SwitchToBackgroundAsync();
await userDbService.DeleteUserByIdAsync(user.Entity.InnerId).ConfigureAwait(false);
userDbService.DeleteUserById(user.Entity.InnerId);
messenger.Send(new UserRemovedMessage(user.Entity));
}
// Sync cache
await taskContext.SwitchToMainThreadAsync();
users.Remove(user);
public UserGameRole? GetUserGameRoleByUid(string uid)
{
if (uidUserGameRoleMap is null)
{
return default;
}
return uidUserGameRoleMap.GetValueOrDefault(uid);
}
public bool TryGetUserByMid(string mid, [NotNullWhen(true)] out BindingUser? user)
{
ArgumentNullException.ThrowIfNull(midUserMap);
return midUserMap.TryGetValue(mid, out user);
messenger.Send(new UserRemovedMessage(user));
}
public async ValueTask<ValueResult<UserOptionResult, string>> TryCreateAndAddUserFromInputCookieAsync(InputCookie inputCookie)
@@ -127,25 +78,12 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
return new(UserOptionResult.CookieInvalid, SH.ServiceUserProcessCookieRequestUserInfoFailed);
}
await GetUserCollectionAsync().ConfigureAwait(false);
await GetUsersAsync().ConfigureAwait(false);
ArgumentNullException.ThrowIfNull(users);
// Sync cache
await taskContext.SwitchToMainThreadAsync();
users.Add(newUser); // Database synced in the collection
if (newUser.Entity.Mid is not null)
{
midUserMap?.Add(newUser.Entity.Mid, newUser);
}
if (userAndUidCollection is not null)
{
foreach (UserGameRole role in newUser.UserGameRoles)
{
userAndUidCollection.Add(new(newUser.Entity, role));
uidUserGameRoleMap?.Add(role.GameUid, role);
}
}
ArgumentNullException.ThrowIfNull(newUser.UserInfo);
return new(UserOptionResult.Added, newUser.UserInfo.Uid);
@@ -155,4 +93,41 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
{
throttler.Dispose();
}
private void OnCurrentUserChanged(object? sender, object? args)
{
if (users is null)
{
messenger.Send(UserAndUidChangedMessage.Empty);
return;
}
if (users.CurrentItem is null)
{
messenger.Send(UserAndUidChangedMessage.Empty);
return;
}
// Suppress the BindingUser itself to raise the message
// This is to avoid the message being raised in the
// BindingUser.OnCurrentUserGameRoleChanged.
using (users.CurrentItem.SuppressCurrentUserGameRoleChangedMessage())
{
foreach (UserGameRole role in users.CurrentItem.UserGameRoles)
{
if (role.GameUid == users.CurrentItem.PreferredUid)
{
users.CurrentItem.UserGameRoles.MoveCurrentTo(role);
break;
}
}
if (users.CurrentItem.UserGameRoles.CurrentItem is null)
{
users.CurrentItem.UserGameRoles.MoveCurrentToFirst();
}
}
messenger.Send(new UserAndUidChangedMessage(users.CurrentItem));
}
}

View File

@@ -2,8 +2,7 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service.User;
@@ -13,48 +12,30 @@ internal sealed partial class UserDbService : IUserDbService
{
private readonly IServiceProvider serviceProvider;
public async ValueTask DeleteUserByIdAsync(Guid id)
public IServiceProvider ServiceProvider { get => serviceProvider; }
public void DeleteUserById(Guid id)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.Users.Where(u => u.InnerId == id).ExecuteDeleteAsync().ConfigureAwait(false);
}
this.DeleteByInnerId(id);
}
public async ValueTask<List<Model.Entity.User>> GetUserListAsync()
public List<Model.Entity.User> GetUserList()
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.Users.AsNoTracking().ToListAsync().ConfigureAwait(false);
}
return this.List();
}
public async ValueTask UpdateUserAsync(Model.Entity.User user)
public void UpdateUser(Model.Entity.User user)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.Users.UpdateAndSaveAsync(user).ConfigureAwait(false);
}
this.Update(user);
}
public async ValueTask RemoveUsersAsync()
public void RemoveAllUsers()
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.Users.ExecuteDeleteAsync().ConfigureAwait(false);
}
this.Delete();
}
public async ValueTask ClearUserSelectionAsync()
public void ClearUserSelection()
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.Users.ExecuteUpdateAsync(update => update.SetProperty(user => user.IsSelected, user => false)).ConfigureAwait(false);
}
this.Execute(dbset => dbset.ExecuteUpdate(update => update.SetProperty(user => user.IsSelected, user => false)));
}
}

View File

@@ -18,6 +18,7 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
private readonly IUserFingerprintService userFingerprintService;
private readonly IProfilePictureService profilePictureService;
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
public async ValueTask<ViewModel.User.User> ResumeUserAsync(Model.Entity.User inner, CancellationToken token = default)
{
@@ -210,7 +211,8 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
if (userGameRolesResponse.IsOk())
{
user.UserGameRoles = userGameRolesResponse.Data.List;
await taskContext.SwitchToMainThreadAsync();
user.UserGameRoles = new(userGameRolesResponse.Data.List, true);
return user.UserGameRoles.Count > 0;
}
else

View File

@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using BindingUser = Snap.Hutao.ViewModel.User.User;
namespace Snap.Hutao.Service.User;
internal sealed class UserRemovedMessage
{
public UserRemovedMessage(BindingUser removedUser)
{
RemovedUser = removedUser;
}
public BindingUser RemovedUser { get; }
}

View File

@@ -8,8 +8,6 @@ using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Passport;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Response;
using System.Collections.ObjectModel;
using Windows.Foundation;
using BindingUser = Snap.Hutao.ViewModel.User.User;
using EntityUser = Snap.Hutao.Model.Entity.User;
@@ -33,22 +31,12 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
public async ValueTask UnsafeRemoveAllUsersAsync()
{
await taskContext.SwitchToBackgroundAsync();
await userDbService.RemoveUsersAsync().ConfigureAwait(false);
userDbService.RemoveAllUsers();
}
public ValueTask<AdvancedDbCollectionView<BindingUser, EntityUser>> GetUserCollectionAsync()
public ValueTask<AdvancedDbCollectionView<BindingUser, EntityUser>> GetUsersAsync()
{
return userCollectionService.GetUserCollectionAsync();
}
public ValueTask<ObservableCollection<UserAndUid>> GetRoleCollectionAsync()
{
return userCollectionService.GetUserAndUidCollectionAsync();
}
public UserGameRole? GetUserGameRoleByUid(string uid)
{
return userCollectionService.GetUserGameRoleByUid(uid);
return userCollectionService.GetUsersAsync();
}
public async ValueTask<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(InputCookie inputCookie)
@@ -64,7 +52,7 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
}
// 检查 mid 对应用户是否存在
if (!userCollectionService.TryGetUserByMid(midOrAid, out BindingUser? user))
if (await this.GetUserByMidAsync(midOrAid).ConfigureAwait(false) is not { } user)
{
return await userCollectionService.TryCreateAndAddUserFromInputCookieAsync(inputCookie).ConfigureAwait(false);
}
@@ -79,11 +67,11 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
user.CookieToken = cookie.TryGetCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken;
user.TryUpdateFingerprint(deviceFp);
await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false);
userDbService.UpdateUser(user.Entity);
return new(UserOptionResult.CookieUpdated, midOrAid);
}
public async ValueTask<bool> RefreshCookieTokenAsync(Model.Entity.User user)
public async ValueTask<bool> RefreshCookieTokenAsync(EntityUser user)
{
// TODO: 提醒其他组件此用户的Cookie已更改
Response<UidCookieToken> cookieTokenResponse;
@@ -108,9 +96,8 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
// Check null and create a new one to avoid System.NullReferenceException
user.CookieToken ??= new();
// Sync ui and database
user.CookieToken[Cookie.COOKIE_TOKEN] = cookieToken;
await userDbService.UpdateUserAsync(user).ConfigureAwait(false);
userDbService.UpdateUser(user);
return true;
}

View File

@@ -1,7 +1,11 @@
// 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 BindingUser = Snap.Hutao.ViewModel.User.User;
using EntityUser = Snap.Hutao.Model.Entity.User;
namespace Snap.Hutao.Service.User;
@@ -11,4 +15,40 @@ internal static class UserServiceExtension
{
return userService.RefreshCookieTokenAsync(user.Entity);
}
public static async ValueTask<UserGameRole?> GetUserGameRoleByUidAsync(this IUserService userService, string uid)
{
AdvancedDbCollectionView<BindingUser, EntityUser> users = await userService.GetUsersAsync().ConfigureAwait(false);
foreach (BindingUser user in users.SourceCollection)
{
foreach (UserGameRole role in user.UserGameRoles)
{
if (role.GameUid == uid)
{
return role;
}
}
}
return null;
}
public static async ValueTask<string?> GetCurrentUidAsync(this IUserService userService)
{
AdvancedDbCollectionView<BindingUser, EntityUser> users = await userService.GetUsersAsync().ConfigureAwait(false);
return users.CurrentItem?.UserGameRoles?.CurrentItem?.GameUid;
}
public static async ValueTask<UserAndUid?> GetCurrentUserAndUidAsync(this IUserService userService)
{
AdvancedDbCollectionView<BindingUser, EntityUser> users = await userService.GetUsersAsync().ConfigureAwait(false);
UserAndUid.TryFromUser(users.CurrentItem, out UserAndUid? userAndUid);
return userAndUid;
}
public static async ValueTask<BindingUser?> GetUserByMidAsync(this IUserService userService, string mid)
{
AdvancedDbCollectionView<BindingUser, EntityUser> users = await userService.GetUsersAsync().ConfigureAwait(false);
return users.SourceCollection.SingleOrDefault(u => u.Entity.Mid == mid);
}
}

View File

@@ -15,6 +15,7 @@ namespace Snap.Hutao.UI.Xaml.View.Dialog;
[DependencyProperty("IsUidAttached", typeof(bool))]
internal sealed partial class CultivateProjectDialog : ContentDialog
{
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
/// <summary>
@@ -25,6 +26,7 @@ internal sealed partial class CultivateProjectDialog : ContentDialog
{
InitializeComponent();
this.serviceProvider = serviceProvider;
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
}
@@ -39,7 +41,7 @@ internal sealed partial class CultivateProjectDialog : ContentDialog
if (result == ContentDialogResult.Primary)
{
string? uid = IsUidAttached
? Ioc.Default.GetRequiredService<IUserService>().Current?.SelectedUserGameRole?.GameUid
? await serviceProvider.GetRequiredService<IUserService>().GetCurrentUidAsync().ConfigureAwait(false)
: null;
return new(true, CultivateProject.From(Text, uid));

View File

@@ -17,9 +17,6 @@ internal sealed partial class MainView : UserControl
{
private readonly INavigationService navigationService;
/// <summary>
/// 构造一个新的主视图
/// </summary>
public MainView()
{
IServiceProvider serviceProvider = Ioc.Default;
@@ -28,7 +25,7 @@ internal sealed partial class MainView : UserControl
InitializeComponent();
this.Unloaded += OnUnloaded;
Unloaded += OnUnloaded;
(DataContext as MainViewModel)?.Initialize(new BackgroundImagePresenterAccessor(BackgroundImagePresenter));

View File

@@ -354,7 +354,7 @@
<cwcont:SettingsCard
ActionIconToolTip="{shuxm:ResourceString Name=ViewPageCultivationNavigateAction}"
Command="{Binding NavigateToPageCommand}"
CommandParameter="Snap.Hutao.View.Page.WikiAvatarPage"
CommandParameter="Snap.Hutao.UI.Xaml.View.Page.WikiAvatarPage"
Description="{shuxm:ResourceString Name=ViewPageCultivationWikiAvatarDescription}"
Header="{shuxm:ResourceString Name=ViewWikiAvatarHeader}"
HeaderIcon="{shuxm:BitmapIcon Source=ms-appx:///Resource/Navigation/WikiAvatar.png}"
@@ -362,7 +362,7 @@
<cwcont:SettingsCard
ActionIconToolTip="{shuxm:ResourceString Name=ViewPageCultivationNavigateAction}"
Command="{Binding NavigateToPageCommand}"
CommandParameter="Snap.Hutao.View.Page.WikiWeaponPage"
CommandParameter="Snap.Hutao.UI.Xaml.View.Page.WikiWeaponPage"
Description="{shuxm:ResourceString Name=ViewPageCultivationWikiWeaponDescription}"
Header="{shuxm:ResourceString Name=ViewWikiWeaponHeader}"
HeaderIcon="{shuxm:BitmapIcon Source=ms-appx:///Resource/Navigation/WikiWeapon.png}"
@@ -370,7 +370,7 @@
<cwcont:SettingsCard
ActionIconToolTip="{shuxm:ResourceString Name=ViewPageCultivationNavigateAction}"
Command="{Binding NavigateToPageCommand}"
CommandParameter="Snap.Hutao.View.Page.AvatarPropertyPage"
CommandParameter="Snap.Hutao.UI.Xaml.View.Page.AvatarPropertyPage"
Description="{shuxm:ResourceString Name=ViewPageCultivationAvatarPropertyDescription}"
Header="{shuxm:ResourceString Name=ViewAvatarPropertyHeader}"
HeaderIcon="{shuxm:BitmapIcon Source=ms-appx:///Resource/Navigation/Cultivation.png}"

View File

@@ -40,23 +40,6 @@
FalseValue="{StaticResource UI_MarkQuest_Main_Start}"
TrueValue="{StaticResource UI_MarkQuest_Main_Proce}"/>
<DataTemplate x:Key="UserAndUidTemplate">
<Grid Padding="0,0,0,16">
<TextBlock VerticalAlignment="Center" Text="{Binding Uid}"/>
<Button
Margin="16,0,0,0"
Padding="6"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding DataContext.TrackRoleCommand, Source={StaticResource ViewModelBindingProxy}}"
CommandParameter="{Binding}"
Content="&#xE710;"
FontFamily="{StaticResource SymbolThemeFontFamily}"
Style="{StaticResource ButtonRevealStyle}"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewPageDailyNoteAddEntryToolTip}"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="DailyNoteEntryTemplate" x:DataType="shme:DailyNoteEntry">
<ItemContainer cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<ItemContainer.Resources>
@@ -460,24 +443,10 @@
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors>
</AppBarButton>
<AppBarButton Icon="{shuxm:FontIcon Glyph={StaticResource FontIconContentAdd}}" Label="{shuxm:ResourceString Name=ViewPageDailyNoteAddEntry}">
<AppBarButton.Flyout>
<Flyout
FlyoutPresenterStyle="{StaticResource FlyoutPresenterPadding0And2Style}"
LightDismissOverlayMode="On"
Placement="Bottom">
<StackPanel>
<TextBlock
Margin="16,12,16,16"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shuxm:ResourceString Name=ViewPageDailyNoteAddEntryHint}"/>
<ScrollViewer MaxHeight="320" Padding="16,0">
<ItemsControl ItemTemplate="{StaticResource UserAndUidTemplate}" ItemsSource="{Binding UserAndUids}"/>
</ScrollViewer>
</StackPanel>
</Flyout>
</AppBarButton.Flyout>
</AppBarButton>
<AppBarButton
Command="{Binding TrackCurrentUserAndUidCommand}"
Icon="{shuxm:FontIcon Glyph={StaticResource FontIconContentAdd}}"
Label="{shuxm:ResourceString Name=ViewPageDailyNoteAddEntry}"/>
<AppBarButton Icon="{shuxm:FontIcon Glyph={StaticResource FontIconContentSetting}}" Label="{shuxm:ResourceString Name=ViewPageDailyNoteNotificationSetting}">
<AppBarButton.Flyout>
<Flyout Placement="BottomEdgeAlignedRight">

View File

@@ -38,9 +38,8 @@ internal interface ISupportLoginByWebView
serviceProvider.GetRequiredService<INavigationService>().GoBack();
await serviceProvider
serviceProvider
.GetRequiredService<ViewModel.User.UserViewModel>()
.HandleUserOptionResultAsync(result, nickname)
.ConfigureAwait(false);
.HandleUserOptionResult(result, nickname);
}
}

View File

@@ -100,6 +100,37 @@
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<Grid.Resources>
<Storyboard x:Name="ButtonPanelVisibleStoryboard">
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonPanel" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Name="ButtonPanelCollapsedStoryboard">
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonPanel" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</Grid.Resources>
<mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="PointerEntered">
<mxim:ControlStoryboardAction Storyboard="{StaticResource ButtonPanelVisibleStoryboard}"/>
</mxic:EventTriggerBehavior>
<mxic:EventTriggerBehavior EventName="PointerExited">
<mxim:ControlStoryboardAction Storyboard="{StaticResource ButtonPanelCollapsedStoryboard}"/>
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors>
<PersonPicture
Height="32"
Margin="2,0"
@@ -142,37 +173,6 @@
Style="{StaticResource ButtonRevealStyle}"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ViewUserRemoveAction}"/>
</StackPanel>
<Grid.Resources>
<Storyboard x:Name="ButtonPanelVisibleStoryboard">
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonPanel" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Name="ButtonPanelCollapsedStoryboard">
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonPanel" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</Grid.Resources>
<mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="PointerEntered">
<mxim:ControlStoryboardAction Storyboard="{StaticResource ButtonPanelVisibleStoryboard}"/>
</mxic:EventTriggerBehavior>
<mxic:EventTriggerBehavior EventName="PointerExited">
<mxim:ControlStoryboardAction Storyboard="{StaticResource ButtonPanelCollapsedStoryboard}"/>
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors>
</Grid>
</DataTemplate>
</UserControl.Resources>
@@ -236,12 +236,12 @@
Height="36"
Margin="1,1,6,1"
HorizontalAlignment="Left"
ProfilePicture="{Binding SelectedUser.UserInfo.AvatarUri, Mode=OneWay}"/>
ProfilePicture="{Binding Users.CurrentItem.UserInfo.AvatarUri, Mode=OneWay}"/>
<TextBlock
Grid.Column="1"
Margin="1,0,0,0"
VerticalAlignment="Center"
Text="{Binding SelectedUser.UserInfo.Nickname, Mode=OneWay, FallbackValue={shuxm:ResourceString Name=ViewUserNoUserHint}}"
Text="{Binding Users.CurrentItem.UserInfo.Nickname, Mode=OneWay, FallbackValue={shuxm:ResourceString Name=ViewUserNoUserHint}}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"/>
<FontIcon
@@ -384,7 +384,7 @@
<Grid Grid.Column="1" Width="280">
<StackPanel Visibility="{Binding Users.Count, Converter={StaticResource Int32ToVisibilityConverter}}">
<StackPanel Visibility="{Binding SelectedUser, Converter={StaticResource EmptyObjectToVisibilityConverter}, Mode=OneWay}">
<StackPanel Visibility="{Binding Users.CurrentItem, Converter={StaticResource EmptyObjectToVisibilityConverter}, Mode=OneWay}">
<TextBlock
Margin="10,6,0,6"
Style="{StaticResource BaseTextBlockStyle}"
@@ -393,8 +393,8 @@
Grid.Row="1"
Margin="4"
ItemTemplate="{StaticResource UserGameRoleTemplate}"
ItemsSource="{Binding SelectedUser.UserGameRoles}"
SelectedItem="{Binding SelectedUser.SelectedUserGameRole, Mode=TwoWay}"
ItemsSource="{Binding Users.CurrentItem.UserGameRoles}"
SelectedItem="{Binding Users.CurrentItem.UserGameRoles.CurrentItem, Mode=TwoWay}"
SelectionMode="Single"/>
</StackPanel>
@@ -409,7 +409,7 @@
CanReorderItems="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolNegationConverter}}"
ItemTemplate="{StaticResource UserTemplate}"
ItemsSource="{Binding Users}"
SelectedItem="{Binding SelectedUser, Mode=TwoWay}"
SelectedItem="{Binding Users.CurrentItem, Mode=TwoWay}"
SelectionMode="Single">
<ListView.Header>
<InfoBar

View File

@@ -5,7 +5,6 @@ using Microsoft.UI.Xaml;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Bridge;
using Windows.Graphics;
@@ -29,14 +28,8 @@ internal sealed partial class MiHoYoJSBridgeWebView2ContentProvider : Dependency
return;
}
User? user = serviceProvider.GetRequiredService<IUserService>().Current;
if (user is null || user.SelectedUserGameRole is null)
{
return;
}
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
if (!UserAndUid.TryFromUser(user, out UserAndUid? userAndUid))
if (await serviceProvider.GetRequiredService<IUserService>().GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
{
infoBarService.Warning(SH.MustSelectUserAndUid);
return;
@@ -59,7 +52,7 @@ internal sealed partial class MiHoYoJSBridgeWebView2ContentProvider : Dependency
}
CoreWebView2
.SetCookie(user.CookieToken, user.LToken, userAndUid.IsOversea)
.SetCookie(userAndUid.User.CookieToken, userAndUid.User.LToken, userAndUid.IsOversea)
.SetMobileUserAgent(userAndUid.IsOversea);
jsBridge = SourceProvider.CreateJSBridge(serviceProvider, CoreWebView2, userAndUid);

View File

@@ -4,7 +4,6 @@
using Snap.Hutao.Core.DataTransfer;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Picker;
using Snap.Hutao.Service.Achievement;
using Snap.Hutao.Service.Notification;
namespace Snap.Hutao.ViewModel.Achievement;

View File

@@ -9,7 +9,6 @@ using Snap.Hutao.Core.DataTransfer;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.Graphics.Imaging;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Message;
using Snap.Hutao.Model.Calculable;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.AvatarInfo;
@@ -40,7 +39,7 @@ namespace Snap.Hutao.ViewModel.AvatarProperty;
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, IRecipient<UserChangedMessage>
internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, IRecipient<UserAndUidChangedMessage>
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly IAppResourceProvider appResourceProvider;
@@ -73,9 +72,9 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
public AvatarView? SelectedAvatar { get => selectedAvatar; set => SetProperty(ref selectedAvatar, value); }
/// <inheritdoc/>
public void Receive(UserChangedMessage message)
public void Receive(UserAndUidChangedMessage message)
{
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (message.UserAndUid is { } userAndUid)
{
RefreshCoreAsync(userAndUid, RefreshOption.None, CancellationToken).SafeForget();
}
@@ -83,7 +82,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
protected override async ValueTask<bool> InitializeOverrideAsync()
{
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
{
await RefreshCoreAsync(userAndUid, RefreshOption.None, CancellationToken).ConfigureAwait(false);
return true;
@@ -95,7 +94,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
[Command("RefreshFromEnkaApiCommand")]
private async Task RefreshByEnkaApiAsync()
{
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
{
await RefreshCoreAsync(userAndUid, RefreshOption.RequestFromEnkaAPI, CancellationToken).ConfigureAwait(false);
}
@@ -104,7 +103,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
[Command("RefreshFromHoyolabGameRecordCommand")]
private async Task RefreshByHoyolabGameRecordAsync()
{
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
{
await RefreshCoreAsync(userAndUid, RefreshOption.RequestFromHoyolabGameRecord, CancellationToken).ConfigureAwait(false);
}
@@ -113,7 +112,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
[Command("RefreshFromHoyolabCalculateCommand")]
private async Task RefreshByHoyolabCalculateAsync()
{
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
{
await RefreshCoreAsync(userAndUid, RefreshOption.RequestFromHoyolabCalculate, CancellationToken).ConfigureAwait(false);
}
@@ -177,7 +176,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
return;
}
if (!UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
{
infoBarService.Warning(SH.MustSelectUserAndUid);
return;
@@ -222,7 +221,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
return;
}
if (!UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
{
infoBarService.Warning(SH.MustSelectUserAndUid);
return;

View File

@@ -3,6 +3,7 @@
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Model.Entity;
@@ -15,14 +16,11 @@ using Snap.Hutao.UI.Xaml.Control;
using Snap.Hutao.UI.Xaml.View.Dialog;
using Snap.Hutao.UI.Xaml.View.Window.WebView2;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
namespace Snap.Hutao.ViewModel.DailyNote;
/// <summary>
/// 实时便笺视图模型
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel
@@ -37,7 +35,7 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel
private readonly IUserService userService;
private readonly AppOptions appOptions;
private ObservableCollection<UserAndUid>? userAndUids;
private AdvancedDbCollectionView<ViewModel.User.User, Model.Entity.User>? users;
private ObservableCollection<DailyNoteEntry>? dailyNoteEntries;
public DailyNoteOptions DailyNoteOptions { get => dailyNoteOptions; }
@@ -48,14 +46,8 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel
public IJSBridgeUriSourceProvider VerifyUrlSource { get; } = new DailyJSBridgeUriSourceProvider();
/// <summary>
/// 用户与角色集合
/// </summary>
public ObservableCollection<UserAndUid>? UserAndUids { get => userAndUids; set => SetProperty(ref userAndUids, value); }
public AdvancedDbCollectionView<User.User, Model.Entity.User>? Users { get => users; set => SetProperty(ref users, value); }
/// <summary>
/// 实时便笺集合
/// </summary>
public ObservableCollection<DailyNoteEntry>? DailyNoteEntries { get => dailyNoteEntries; set => SetProperty(ref dailyNoteEntries, value); }
protected override async ValueTask<bool> InitializeOverrideAsync()
@@ -65,11 +57,11 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel
try
{
await taskContext.SwitchToBackgroundAsync();
ObservableCollection<UserAndUid> roles = await userService.GetRoleCollectionAsync().ConfigureAwait(false);
AdvancedDbCollectionView<User.User, Model.Entity.User> users = await userService.GetUsersAsync().ConfigureAwait(false);
ObservableCollection<DailyNoteEntry> entries = await dailyNoteService.GetDailyNoteEntryCollectionAsync().ConfigureAwait(false);
await taskContext.SwitchToMainThreadAsync();
UserAndUids = roles;
Users = users;
DailyNoteEntries = entries;
return true;
}
@@ -82,19 +74,22 @@ internal sealed partial class DailyNoteViewModel : Abstraction.ViewModel
return false;
}
[Command("TrackRoleCommand")]
private async Task TrackRoleAsync(UserAndUid? userAndUid)
[Command("TrackCurrentUserAndUidCommand")]
private async Task TrackCurrentUserAndUidAsync()
{
if (userAndUid is not null)
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
{
ContentDialog dialog = await contentDialogFactory
.CreateForIndeterminateProgressAsync(SH.ViewModelDailyNoteRequestProgressTitle)
.ConfigureAwait(false);
infoBarService.Warning(SH.MustSelectUserAndUid);
return;
}
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{
await dailyNoteService.AddDailyNoteAsync(userAndUid).ConfigureAwait(false);
}
ContentDialog dialog = await contentDialogFactory
.CreateForIndeterminateProgressAsync(SH.ViewModelDailyNoteRequestProgressTitle)
.ConfigureAwait(false);
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{
await dailyNoteService.AddDailyNoteAsync(userAndUid).ConfigureAwait(false);
}
}

View File

@@ -240,16 +240,16 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
}
[Command("AttachGameAccountCommand")]
private void AttachGameAccountToCurrentUserGameRole(GameAccount? gameAccount)
private async Task AttachGameAccountToCurrentUserGameRole(GameAccount? gameAccount)
{
if (gameAccount is null)
{
return;
}
if (userService.Current?.SelectedUserGameRole is { } role)
if (await userService.GetCurrentUidAsync().ConfigureAwait(false) is { } uid)
{
gameService.AttachGameAccountToUid(gameAccount, role.GameUid);
gameService.AttachGameAccountToUid(gameAccount, uid);
}
else
{

View File

@@ -1,11 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI.Animations;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using Snap.Hutao.Message;
using Snap.Hutao.Service;
using Snap.Hutao.Service.BackgroundImage;
using Snap.Hutao.UI.Xaml.Control.Theme;

View File

@@ -4,7 +4,6 @@
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Message;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.Notification;
@@ -27,7 +26,7 @@ namespace Snap.Hutao.ViewModel.SpiralAbyss;
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped)]
internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecipient<UserChangedMessage>
internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecipient<UserAndUidChangedMessage>
{
private readonly ISpiralAbyssRecordService spiralAbyssRecordService;
private readonly IContentDialogFactory contentDialogFactory;
@@ -54,10 +53,9 @@ internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel
public HutaoDatabaseViewModel HutaoDatabaseViewModel { get => hutaoDatabaseViewModel; }
/// <inheritdoc/>
public void Receive(UserChangedMessage message)
public void Receive(UserAndUidChangedMessage message)
{
if (UserAndUid.TryFromUser(message.NewValue, out UserAndUid? userAndUid))
if (message.UserAndUid is { } userAndUid)
{
UpdateSpiralAbyssCollectionAsync(userAndUid).SafeForget();
}
@@ -71,7 +69,7 @@ internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel
{
if (await spiralAbyssRecordService.InitializeAsync().ConfigureAwait(false))
{
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
{
await UpdateSpiralAbyssCollectionAsync(userAndUid).ConfigureAwait(false);
return true;
@@ -111,7 +109,7 @@ internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel
{
if (SpiralAbyssEntries is not null)
{
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
{
try
{
@@ -135,7 +133,7 @@ internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel
[Command("UploadSpiralAbyssRecordCommand")]
private async Task UploadSpiralAbyssRecordAsync()
{
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is { } userAndUid)
{
if (!hutaoUserOptions.IsLoggedIn)
{

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Database;
@@ -16,8 +15,7 @@ using EntityUser = Snap.Hutao.Model.Entity.User;
namespace Snap.Hutao.ViewModel.User;
internal sealed class User : ObservableObject,
IEntityAccess<EntityUser>,
internal sealed class User : IEntityAccess<EntityUser>,
IMappingFrom<User, EntityUser, IServiceProvider>,
ISelectable,
IAdvancedCollectionViewItem
@@ -25,7 +23,8 @@ internal sealed class User : ObservableObject,
private readonly EntityUser inner;
private readonly IServiceProvider serviceProvider;
private UserGameRole? selectedUserGameRole;
private AdvancedCollectionView<UserGameRole> userGameRoles = default!;
private bool isCurrentUserGameRoleChangedMessageSuppressed;
private User(EntityUser user, IServiceProvider serviceProvider)
{
@@ -37,12 +36,23 @@ internal sealed class User : ObservableObject,
public UserInfo? UserInfo { get; set; }
public List<UserGameRole> UserGameRoles { get; set; } = default!;
public UserGameRole? SelectedUserGameRole
public AdvancedCollectionView<UserGameRole> UserGameRoles
{
get => selectedUserGameRole;
set => SetSelectedUserGameRole(value);
get => userGameRoles;
set
{
if (userGameRoles is not null)
{
userGameRoles.CurrentChanged -= OnCurrentUserGameRoleChanged;
}
userGameRoles = value;
if (value is not null)
{
value.CurrentChanged += OnCurrentUserGameRoleChanged;
}
}
}
public string? Fingerprint { get => inner.Fingerprint; }
@@ -86,26 +96,6 @@ internal sealed class User : ObservableObject,
return new(user, provider);
}
public void SetSelectedUserGameRole(UserGameRole? value, bool raiseMessage = true)
{
if (SetProperty(ref selectedUserGameRole, value, nameof(SelectedUserGameRole)))
{
if (value is not null && inner.PreferredUid != value.GameUid)
{
inner.PreferredUid = value.GameUid;
using (IServiceScope scope = serviceProvider.CreateScope())
{
scope.ServiceProvider.GetRequiredService<AppDbContext>().Users.UpdateAndSave(inner);
}
}
if (raiseMessage)
{
serviceProvider.GetRequiredService<IMessenger>().Send(Message.UserChangedMessage.CreateOnlyRoleChanged(this));
}
}
}
public object? GetPropertyValue(string name)
{
return name switch
@@ -113,4 +103,42 @@ internal sealed class User : ObservableObject,
_ => default,
};
}
public IDisposable SuppressCurrentUserGameRoleChangedMessage()
{
return new CurrentUserGameRoleChangedSuppression(this);
}
private void OnCurrentUserGameRoleChanged(object? sender, object? e)
{
if (userGameRoles.CurrentItem is { } item && inner.PreferredUid != item.GameUid)
{
inner.PreferredUid = item.GameUid;
using (IServiceScope scope = serviceProvider.CreateScope())
{
scope.ServiceProvider.GetRequiredService<AppDbContext>().Users.UpdateAndSave(inner);
}
}
if (!isCurrentUserGameRoleChangedMessageSuppressed)
{
serviceProvider.GetRequiredService<IMessenger>().Send(new UserAndUidChangedMessage(this));
}
}
private sealed class CurrentUserGameRoleChangedSuppression : IDisposable
{
private readonly User reference;
public CurrentUserGameRoleChangedSuppression(User reference)
{
this.reference = reference;
reference.isCurrentUserGameRoleChangedMessageSuppressed = true;
}
public void Dispose()
{
reference.isCurrentUserGameRoleChangedMessageSuppressed = false;
}
}
}

View File

@@ -7,33 +7,16 @@ using EntityUser = Snap.Hutao.Model.Entity.User;
namespace Snap.Hutao.ViewModel.User;
/// <summary>
/// 实体用户与角色
/// 由于许多操作需要同时用到ck与uid
/// 抽象此类用于简化这类调用
/// </summary>
[HighQuality]
internal sealed class UserAndUid : IMappingFrom<UserAndUid, EntityUser, PlayerUid>
{
/// <summary>
/// 构造一个新的实体用户与角色
/// </summary>
/// <param name="user">实体用户</param>
/// <param name="role">角色</param>
public UserAndUid(EntityUser user, in PlayerUid role)
{
User = user;
Uid = role;
}
/// <summary>
/// 实体用户
/// </summary>
public EntityUser User { get; private set; }
/// <summary>
/// 角色
/// </summary>
public PlayerUid Uid { get; private set; }
public bool IsOversea { get => User.IsOversea; }
@@ -43,17 +26,11 @@ internal sealed class UserAndUid : IMappingFrom<UserAndUid, EntityUser, PlayerUi
return new(user, role);
}
/// <summary>
/// 尝试转换到用户与角色
/// </summary>
/// <param name="user">用户</param>
/// <param name="userAndUid">用户与角色</param>
/// <returns>是否转换成功</returns>
public static bool TryFromUser([NotNullWhen(true)] User? user, [NotNullWhen(true)] out UserAndUid? userAndUid)
{
if (user is not null && user.SelectedUserGameRole is not null)
if (user is { UserGameRoles.CurrentItem: { } role })
{
userAndUid = new UserAndUid(user.Entity, user.SelectedUserGameRole);
userAndUid = new UserAndUid(user.Entity, role);
return true;
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.ViewModel.User;
internal sealed class UserAndUidChangedMessage
{
public static readonly UserAndUidChangedMessage Empty = new(null);
public UserAndUidChangedMessage(User? user)
{
User = user;
if (UserAndUid.TryFromUser(user, out UserAndUid? userAndUid))
{
UserAndUid = userAndUid;
}
}
public User? User { get; set; }
public UserAndUid? UserAndUid { get; }
public static UserAndUidChangedMessage FromUser(User? user)
{
return new(user);
}
}

View File

@@ -40,66 +40,18 @@ internal sealed partial class UserViewModel : ObservableObject
private readonly ITaskContext taskContext;
private readonly IUserService userService;
private User? selectedUser;
private ObservableReorderableDbCollection<User, EntityUser>? users;
private AdvancedDbCollectionView<User, EntityUser>? users;
public RuntimeOptions RuntimeOptions { get => runtimeOptions; }
/// <summary>
/// 当前选择的用户信息
/// </summary>
public User? SelectedUser
{
get => selectedUser ??= userService.Current;
set
{
if (value is not null)
{
// Should not raise propery changed event below
if (value.PreferredUid is not null)
{
value.SetSelectedUserGameRole(value.UserGameRoles.FirstOrDefault(role => role.GameUid == value.PreferredUid), false);
}
public AdvancedDbCollectionView<User, EntityUser>? Users { get => users; set => SetProperty(ref users, value); }
if (value.SelectedUserGameRole is null)
{
value.SetSelectedUserGameRole(value.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen), false);
}
}
if (!ReferenceEquals(selectedUser, value))
{
selectedUser = value;
userService.Current = value;
}
OnPropertyChanged(nameof(SelectedUser));
}
}
/// <summary>
/// 用户信息集合
/// </summary>
public ObservableReorderableDbCollection<User, EntityUser>? Users { get => users; set => SetProperty(ref users, value); }
/// <summary>
/// 处理用户操作结果
/// </summary>
/// <param name="optionResult">操作结果</param>
/// <param name="uid">uid</param>
/// <returns>任务</returns>
internal async ValueTask HandleUserOptionResultAsync(UserOptionResult optionResult, string uid)
internal void HandleUserOptionResult(UserOptionResult optionResult, string uid)
{
switch (optionResult)
{
case UserOptionResult.Added:
ArgumentNullException.ThrowIfNull(Users);
if (Users.Count == 1)
{
await taskContext.SwitchToMainThreadAsync();
SelectedUser = Users.Single();
}
infoBarService.Success(SH.FormatViewModelUserAdded(uid));
break;
case UserOptionResult.CookieIncomplete:
@@ -116,13 +68,13 @@ internal sealed partial class UserViewModel : ObservableObject
}
}
[Command("OpenUICommand")]
private async Task OpenUIAsync()
[Command("LoadCommand")]
private async Task LoadAsync()
{
try
{
Users = await userService.GetUserCollectionAsync().ConfigureAwait(true);
SelectedUser = userService.Current;
Users = await userService.GetUsersAsync().ConfigureAwait(true);
Users.MoveCurrentToFirst();
}
catch (HutaoException ex)
{
@@ -156,7 +108,7 @@ internal sealed partial class UserViewModel : ObservableObject
{
Cookie cookie = Cookie.Parse(rawCookie);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(InputCookie.CreateWithDeviceFpInference(cookie, isOversea)).ConfigureAwait(false);
await HandleUserOptionResultAsync(optionResult, uid).ConfigureAwait(false);
HandleUserOptionResult(optionResult, uid);
}
}
@@ -205,7 +157,7 @@ internal sealed partial class UserViewModel : ObservableObject
{
Cookie stokenV2 = Cookie.FromLoginResult(sTokenResponse.Data);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(InputCookie.CreateWithDeviceFpInference(stokenV2, false)).ConfigureAwait(false);
await HandleUserOptionResultAsync(optionResult, uid).ConfigureAwait(false);
HandleUserOptionResult(optionResult, uid);
}
}
@@ -219,9 +171,9 @@ internal sealed partial class UserViewModel : ObservableObject
try
{
if (user.IsSelected)
if (ReferenceEquals(users?.CurrentItem, user))
{
SelectedUser = default;
users.MoveCurrentToFirst();
}
await userService.RemoveUserAsync(user).ConfigureAwait(false);
@@ -260,12 +212,12 @@ internal sealed partial class UserViewModel : ObservableObject
[Command("RefreshCookieTokenCommand")]
private async Task RefreshCookieTokenAsync()
{
if (SelectedUser is null)
if (users?.CurrentItem is null)
{
return;
}
if (await userService.RefreshCookieTokenAsync(SelectedUser).ConfigureAwait(false))
if (await userService.RefreshCookieTokenAsync(users.CurrentItem).ConfigureAwait(false))
{
infoBarService.Success(SH.ViewUserRefreshCookieTokenSuccess);
}
@@ -278,7 +230,7 @@ internal sealed partial class UserViewModel : ObservableObject
[Command("ClaimSignInRewardCommand")]
private async Task ClaimSignInRewardAsync(AppBarButton? appBarButton)
{
if (!UserAndUid.TryFromUser(SelectedUser, out UserAndUid? userAndUid))
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
{
infoBarService.Warning(SH.MustSelectUserAndUid);
return;

View File

@@ -20,7 +20,6 @@ using Snap.Hutao.Service.User;
using Snap.Hutao.UI.Xaml.Control.AutoSuggestBox;
using Snap.Hutao.UI.Xaml.Data;
using Snap.Hutao.UI.Xaml.View.Dialog;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Response;
using System.Collections.Frozen;
using System.Collections.ObjectModel;
@@ -162,7 +161,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
return;
}
if (!UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
{
infoBarService.Warning(SH.MustSelectUserAndUid);
return;

View File

@@ -20,7 +20,6 @@ using Snap.Hutao.Service.User;
using Snap.Hutao.UI.Xaml.Control.AutoSuggestBox;
using Snap.Hutao.UI.Xaml.Data;
using Snap.Hutao.UI.Xaml.View.Dialog;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Response;
using System.Collections.Frozen;
using System.Collections.ObjectModel;
@@ -153,7 +152,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
return;
}
if (!UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
if (await userService.GetCurrentUserAndUidAsync().ConfigureAwait(false) is not { } userAndUid)
{
infoBarService.Warning(SH.MustSelectUserAndUid);
return;

View File

@@ -2,71 +2,40 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Service.User;
using Snap.Hutao.UI.Xaml.Data;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
/// <summary>
/// 用户游戏角色
/// </summary>
[HighQuality]
internal sealed class UserGameRole : ObservableObject
internal sealed partial class UserGameRole : ObservableObject, IAdvancedCollectionViewItem
{
private string? profilePictureIcon;
private ICommand? refreshProfilePictureCommand;
/// <summary>
/// hk4e_cn for Genshin Impact
/// </summary>
[JsonPropertyName("game_biz")]
public string GameBiz { get; set; } = default!;
/// <summary>
/// 服务器
/// </summary>
[JsonPropertyName("region")]
public Region Region { get; set; } = default!;
/// <summary>
/// 游戏Uid
/// </summary>
[JsonPropertyName("game_uid")]
public string GameUid { get; set; } = default!;
/// <summary>
/// 昵称
/// </summary>
[JsonPropertyName("nickname")]
public string Nickname { get; set; } = default!;
/// <summary>
/// 等级
/// </summary>
[JsonPropertyName("level")]
public int Level { get; set; }
/// <summary>
/// 是否选中
/// </summary>
[JsonPropertyName("is_chosen")]
public bool IsChosen { get; set; }
/// <summary>
/// 地区名称
/// </summary>
[JsonPropertyName("region_name")]
public string RegionName { get; set; } = default!;
/// <summary>
/// 是否为官服
/// </summary>
[JsonPropertyName("is_official")]
public bool IsOfficial { get; set; } = default!;
/// <summary>
/// 玩家服务器与等级简述
/// </summary>
[JsonIgnore]
public string Description
{
get => $"{RegionName} | Lv.{Level}";
@@ -79,11 +48,6 @@ internal sealed class UserGameRole : ObservableObject
set => SetProperty(ref profilePictureIcon, value);
}
public ICommand RefreshProfilePictureCommand
{
get => refreshProfilePictureCommand ??= new AsyncRelayCommand(RefreshProfilePictureAsync);
}
public static implicit operator PlayerUid(UserGameRole userGameRole)
{
return new PlayerUid(userGameRole.GameUid, userGameRole.Region);
@@ -95,7 +59,15 @@ internal sealed class UserGameRole : ObservableObject
return $"{Nickname} | {RegionName} | Lv.{Level}";
}
[SuppressMessage("", "SH003")]
public object? GetPropertyValue(string name)
{
return name switch
{
_ => default,
};
}
[Command("RefreshProfilePictureCommand")]
private async Task RefreshProfilePictureAsync()
{
await Ioc.Default.GetRequiredService<IUserService>().RefreshProfilePictureAsync(this).ConfigureAwait(false);