mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
handle com exception caused by LoadedImageSurface
This commit is contained in:
@@ -11,6 +11,7 @@ using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using System.Diagnostics;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
@@ -54,9 +55,9 @@ public partial class App : Application
|
||||
/// <inheritdoc cref="Windows.Storage.ApplicationData.Current"/>
|
||||
/// </summary>
|
||||
[SuppressMessage("", "CA1822")]
|
||||
public Windows.Storage.ApplicationData AppData
|
||||
public StorageFolder CacheFolder
|
||||
{
|
||||
get => Windows.Storage.ApplicationData.Current;
|
||||
get => ApplicationData.Current.TemporaryFolder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -81,7 +82,8 @@ public partial class App : Application
|
||||
Window = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
Window.Activate();
|
||||
|
||||
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", AppData.TemporaryFolder.Path);
|
||||
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", CacheFolder.Path);
|
||||
logger.LogInformation(EventIds.CommonLog, "Data folder : {folder}", CacheFolder.Path);
|
||||
|
||||
Ioc.Default
|
||||
.GetRequiredService<IMetadataService>()
|
||||
|
||||
@@ -16,7 +16,7 @@ public class AppDbContextDesignTimeFactory : IDesignTimeDbContextFactory<AppDbCo
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public AppDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
MyDocumentContext myDocument = new(new());
|
||||
HutaoContext myDocument = new(new());
|
||||
return AppDbContext.Create($"Data Source={myDocument.Locate("Userdata.db")}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public class LogDbContextDesignTimeFactory : IDesignTimeDbContextFactory<LogDbCo
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public LogDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
MyDocumentContext myDocument = new(new());
|
||||
HutaoContext myDocument = new(new());
|
||||
return LogDbContext.Create($"Data Source={myDocument.Locate("Log.db")}");
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Context.FileSystem.Location;
|
||||
|
||||
namespace Snap.Hutao.Context.FileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// 我的文档上下文
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
internal class MyDocumentContext : FileSystemContext
|
||||
internal class HutaoContext : FileSystemContext
|
||||
{
|
||||
/// <inheritdoc cref="FileSystemContext"/>
|
||||
public MyDocumentContext(MyDocument myDocument)
|
||||
public HutaoContext(Location.HutaoLocation myDocument)
|
||||
: base(myDocument)
|
||||
{
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Context.FileSystem.Location;
|
||||
/// 我的文档位置
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
internal class MyDocument : IFileSystemLocation
|
||||
internal class HutaoLocation : IFileSystemLocation
|
||||
{
|
||||
private string? path;
|
||||
|
||||
@@ -36,6 +36,8 @@ public class CachedImage : ImageEx
|
||||
|
||||
// check token state to determine whether the operation should be canceled.
|
||||
Must.TryThrowOnCanceled(token, "Image source has changed.");
|
||||
|
||||
// return a BitmapImage initialize with a uri will increase image quality.
|
||||
return new BitmapImage(new(file.Path));
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
|
||||
@@ -11,6 +11,7 @@ using Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
@@ -25,6 +26,8 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
private static readonly DependencyProperty SourceProperty = Property<CompositionImage>.Depend(nameof(Source), default(Uri), OnSourceChanged);
|
||||
private static readonly ConcurrentCancellationTokenSource<CompositionImage> LoadingTokenSource = new();
|
||||
|
||||
private readonly IImageCache imageCache;
|
||||
|
||||
private SpriteVisual? spriteVisual;
|
||||
|
||||
/// <summary>
|
||||
@@ -32,6 +35,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
/// </summary>
|
||||
public CompositionImage()
|
||||
{
|
||||
imageCache = Ioc.Default.GetRequiredService<IImageCache>();
|
||||
IsTabStop = false;
|
||||
SizeChanged += OnSizeChanged;
|
||||
}
|
||||
@@ -59,11 +63,19 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
/// <param name="storageFile">文件</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>加载的图像表面</returns>
|
||||
protected virtual async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
|
||||
protected virtual async Task<LoadedImageSurface?> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
|
||||
{
|
||||
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token))
|
||||
try
|
||||
{
|
||||
return LoadedImageSurface.StartLoadFromStream(imageStream);
|
||||
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token))
|
||||
{
|
||||
return LoadedImageSurface.StartLoadFromStream(imageStream);
|
||||
}
|
||||
}
|
||||
catch (COMException ex) when (ex.HResult == unchecked((int)0x88982F50))
|
||||
{
|
||||
// COMException (0x88982F50): 无法找不到组件。
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,11 +88,6 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
spriteVisual.Size = ActualSize;
|
||||
}
|
||||
|
||||
private static Task<StorageFile> GetCachedFileAsync(Uri uri)
|
||||
{
|
||||
return Ioc.Default.GetRequiredService<IImageCache>().GetFileFromCacheAsync(uri);
|
||||
}
|
||||
|
||||
private static void OnApplyImageFailed(Exception exception)
|
||||
{
|
||||
Ioc.Default
|
||||
@@ -90,40 +97,59 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
|
||||
private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs arg)
|
||||
{
|
||||
if (arg.NewValue is Uri uri && uri != (arg.OldValue as Uri) && !string.IsNullOrEmpty(uri.Host))
|
||||
CompositionImage image = (CompositionImage)sender;
|
||||
|
||||
_ = TryGetImageUri(arg, out Uri? uri);
|
||||
ILogger<CompositionImage> logger = Ioc.Default.GetRequiredService<ILogger<CompositionImage>>();
|
||||
image.ApplyImageInternalAsync(uri, LoadingTokenSource.Register(image)).SafeForget(logger, OnApplyImageFailed);
|
||||
}
|
||||
|
||||
private static bool TryGetImageUri(DependencyPropertyChangedEventArgs arg, [NotNullWhen(true)] out Uri? result)
|
||||
{
|
||||
result = null;
|
||||
if (arg.NewValue is Uri inner && !string.IsNullOrEmpty(inner.Host))
|
||||
{
|
||||
CompositionImage image = (CompositionImage)sender;
|
||||
ILogger<CompositionImage> logger = Ioc.Default.GetRequiredService<ILogger<CompositionImage>>();
|
||||
image.ApplyImageInternalAsync(uri, LoadingTokenSource.Register(image)).SafeForget(logger, OnApplyImageFailed);
|
||||
// value is different from old one and not
|
||||
if (inner != (arg.OldValue as Uri))
|
||||
{
|
||||
result = inner;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task ApplyImageInternalAsync(Uri? uri, CancellationToken token)
|
||||
{
|
||||
await AnimationBuilder.Create().Opacity(0d).StartAsync(this, token);
|
||||
|
||||
if (uri is null)
|
||||
if (uri != null)
|
||||
{
|
||||
return;
|
||||
StorageFile storageFile = await imageCache.GetFileFromCacheAsync(uri);
|
||||
|
||||
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
|
||||
|
||||
if (await LoadImageSurfaceAsync(storageFile, token) is LoadedImageSurface imageSurface)
|
||||
{
|
||||
spriteVisual = CompositeSpriteVisual(compositor, imageSurface);
|
||||
OnUpdateVisual(spriteVisual);
|
||||
|
||||
ElementCompositionPreview.SetElementChildVisual(this, spriteVisual);
|
||||
|
||||
await AnimationBuilder.Create().Opacity(1d).StartAsync(this, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Image is broken, remove it
|
||||
await imageCache.RemoveAsync(uri.Enumerate());
|
||||
}
|
||||
}
|
||||
|
||||
StorageFile storageFile = await GetCachedFileAsync(uri);
|
||||
|
||||
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
|
||||
|
||||
LoadedImageSurface imageSurface = await LoadImageSurfaceAsync(storageFile, token);
|
||||
|
||||
spriteVisual = CompositeSpriteVisual(compositor, imageSurface);
|
||||
OnUpdateVisual(spriteVisual);
|
||||
|
||||
ElementCompositionPreview.SetElementChildVisual(this, spriteVisual);
|
||||
|
||||
await AnimationBuilder.Create().Opacity(1d).StartAsync(this, token);
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (e.NewSize != e.PreviousSize && spriteVisual is not null)
|
||||
if (e.NewSize != e.PreviousSize && spriteVisual != null)
|
||||
{
|
||||
OnUpdateVisual(spriteVisual);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public class Gradient : CompositionImage
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
|
||||
protected override async Task<LoadedImageSurface?> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
|
||||
{
|
||||
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token))
|
||||
{
|
||||
|
||||
@@ -63,7 +63,7 @@ public abstract class CacheBase<T>
|
||||
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
|
||||
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
|
||||
|
||||
await InternalClearAsync(files).ConfigureAwait(false);
|
||||
await RemoveAsync(files).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -93,7 +93,7 @@ public abstract class CacheBase<T>
|
||||
}
|
||||
}
|
||||
|
||||
await InternalClearAsync(filesToDelete).ConfigureAwait(false);
|
||||
await RemoveAsync(filesToDelete).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -112,26 +112,19 @@ public abstract class CacheBase<T>
|
||||
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
|
||||
|
||||
List<StorageFile> filesToDelete = new();
|
||||
List<string> keys = new();
|
||||
|
||||
Dictionary<string, StorageFile> hashDictionary = new();
|
||||
|
||||
foreach (StorageFile file in files)
|
||||
{
|
||||
hashDictionary.Add(file.Name, file);
|
||||
}
|
||||
Dictionary<string, StorageFile> cachedFiles = files.ToDictionary(file => file.Name);
|
||||
|
||||
foreach (Uri uri in uriForCachedItems)
|
||||
{
|
||||
string fileName = GetCacheFileName(uri);
|
||||
if (hashDictionary.TryGetValue(fileName, out StorageFile? file))
|
||||
if (cachedFiles.TryGetValue(fileName, out StorageFile? file))
|
||||
{
|
||||
filesToDelete.Add(file);
|
||||
keys.Add(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
await InternalClearAsync(filesToDelete).ConfigureAwait(false);
|
||||
await RemoveAsync(filesToDelete).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -210,22 +203,6 @@ public abstract class CacheBase<T>
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "CA1822")]
|
||||
private async Task InternalClearAsync(IEnumerable<StorageFile> files)
|
||||
{
|
||||
foreach (StorageFile file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
await file.DeleteAsync().AsTask().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Just ignore errors for now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes with default values if user has not initialized explicitly
|
||||
/// </summary>
|
||||
@@ -239,15 +216,17 @@ public abstract class CacheBase<T>
|
||||
|
||||
using (await cacheFolderSemaphore.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
baseFolder ??= ApplicationData.Current.TemporaryFolder;
|
||||
baseFolder ??= App.Current.CacheFolder;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cacheFolderName))
|
||||
{
|
||||
cacheFolderName = GetType().Name;
|
||||
}
|
||||
|
||||
cacheFolder = await baseFolder.CreateFolderAsync(cacheFolderName, CreationCollisionOption.OpenIfExists)
|
||||
.AsTask().ConfigureAwait(false);
|
||||
cacheFolder = await baseFolder
|
||||
.CreateFolderAsync(cacheFolderName, CreationCollisionOption.OpenIfExists)
|
||||
.AsTask()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,4 +239,19 @@ public abstract class CacheBase<T>
|
||||
|
||||
return Must.NotNull(cacheFolder!);
|
||||
}
|
||||
|
||||
private async Task RemoveAsync(IEnumerable<StorageFile> files)
|
||||
{
|
||||
foreach (StorageFile file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
await file.DeleteAsync().AsTask().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
logger.LogError(EventIds.CacheException, "Failed to delete file: {file}", file.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public sealed class DatebaseLoggerProvider : ILoggerProvider
|
||||
// prevent re-entry call
|
||||
if (logDbContext == null)
|
||||
{
|
||||
MyDocumentContext myDocument = new(new());
|
||||
HutaoContext myDocument = new(new());
|
||||
logDbContext = LogDbContext.Create($"Data Source={myDocument.Locate("Log.db")}");
|
||||
if (logDbContext.Database.GetPendingMigrations().Any())
|
||||
{
|
||||
|
||||
@@ -58,6 +58,11 @@ public struct RECT
|
||||
set => Right = value + Left;
|
||||
}
|
||||
|
||||
public long Area
|
||||
{
|
||||
get => Math.BigMul(Width, Height);
|
||||
}
|
||||
|
||||
public System.Drawing.Point Location
|
||||
{
|
||||
get => new(Left, Top);
|
||||
|
||||
@@ -22,4 +22,4 @@ public static class TupleExtensions
|
||||
{
|
||||
return new Dictionary<TKey, TValue>(1) { { tuple.Key, tuple.Value } };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ internal static class IocConfiguration
|
||||
/// <returns>可继续操作的集合</returns>
|
||||
public static IServiceCollection AddDatebase(this IServiceCollection services)
|
||||
{
|
||||
MyDocumentContext myDocument = new(new());
|
||||
HutaoContext myDocument = new(new());
|
||||
|
||||
string dbFile = myDocument.Locate("Userdata.db");
|
||||
string sqlConnectionString = $"Data Source={dbFile}";
|
||||
|
||||
@@ -64,7 +64,7 @@ public sealed partial class MainWindow : Window
|
||||
|
||||
User32.SetWindowText(handle, "胡桃");
|
||||
RECT rect = RetriveWindowRect();
|
||||
if (!rect.Size.IsEmpty)
|
||||
if (rect.Area > 0)
|
||||
{
|
||||
WINDOWPLACEMENT windowPlacement = WINDOWPLACEMENT.Create(new POINT(-1, -1), rect, ShowWindowCommand.Normal);
|
||||
User32.SetWindowPlacement(handle, ref windowPlacement);
|
||||
|
||||
66
src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ElementType.cs
Normal file
66
src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ElementType.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
/// <summary>
|
||||
/// 元素类型
|
||||
/// https://github.com/Grasscutters/Grasscutter/blob/development/src/main/java/emu/grasscutter/game/props/ElementType.java
|
||||
/// </summary>
|
||||
public enum ElementType
|
||||
{
|
||||
/// <summary>
|
||||
/// 无元素
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 火元素
|
||||
/// </summary>
|
||||
Fire = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 水元素
|
||||
/// </summary>
|
||||
Water = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 草元素
|
||||
/// </summary>
|
||||
Grass = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 雷元素
|
||||
/// </summary>
|
||||
Electric = 4,
|
||||
|
||||
/// <summary>
|
||||
/// 冰元素
|
||||
/// </summary>
|
||||
Ice = 5,
|
||||
|
||||
/// <summary>
|
||||
/// 冻元素
|
||||
/// </summary>
|
||||
Frozen = 6,
|
||||
|
||||
/// <summary>
|
||||
/// 风元素
|
||||
/// </summary>
|
||||
Wind = 7,
|
||||
|
||||
/// <summary>
|
||||
/// 岩元素
|
||||
/// </summary>
|
||||
Rock = 8,
|
||||
|
||||
/// <summary>
|
||||
/// 抗火元素
|
||||
/// </summary>
|
||||
AntiFire = 9,
|
||||
|
||||
/// <summary>
|
||||
/// 默认
|
||||
/// </summary>
|
||||
Default = 255,
|
||||
}
|
||||
@@ -43,4 +43,4 @@ public enum EquipType
|
||||
/// 武器
|
||||
/// </summary>
|
||||
EQUIP_WEAPON = 6,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,10 @@ public class SkillDepot
|
||||
/// <summary>
|
||||
/// 全部天赋
|
||||
/// </summary>
|
||||
public IList<ProudableSkill> CompositeSkills => GetCompositeSkills().ToList();
|
||||
public IList<ProudableSkill> CompositeSkills
|
||||
{
|
||||
get => GetCompositeSkills().ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 命之座
|
||||
|
||||
@@ -16,11 +16,11 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
internal class PropertyInfoDescriptor : IValueConverter
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
PropertyInfo rawDescParam = (PropertyInfo)value;
|
||||
|
||||
IList<LevelParam<string, ParameterInfo>> parameters = rawDescParam.Parameters
|
||||
if (value is PropertyInfo rawDescParam)
|
||||
{
|
||||
IList<LevelParam<string, ParameterInfo>> parameters = rawDescParam.Parameters
|
||||
.Select(param =>
|
||||
{
|
||||
IList<ParameterInfo> parameters = GetFormattedParameters(param.Parameters, rawDescParam.Properties);
|
||||
@@ -28,7 +28,10 @@ internal class PropertyInfoDescriptor : IValueConverter
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return parameters;
|
||||
return parameters;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
32
src/Snap.Hutao/Snap.Hutao/Model/Pair.cs
Normal file
32
src/Snap.Hutao/Snap.Hutao/Model/Pair.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 对
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">键的类型</typeparam>
|
||||
/// <typeparam name="TValue">值的类型</typeparam>
|
||||
public class Pair<TKey, TValue>
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的对
|
||||
/// </summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">值</param>
|
||||
public Pair(TKey key, TValue value)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// 键
|
||||
/// </summary>
|
||||
public TKey Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public TValue Value { get; set; }
|
||||
}
|
||||
47
src/Snap.Hutao/Snap.Hutao/Model/Selectable.cs
Normal file
47
src/Snap.Hutao/Snap.Hutao/Model/Selectable.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 可选择的对象
|
||||
/// 默认为选中状态
|
||||
/// </summary>
|
||||
/// <typeparam name="T">值的类型</typeparam>
|
||||
public class Selectable<T> : Observable
|
||||
where T : class
|
||||
{
|
||||
private readonly Action? selectedChanged;
|
||||
|
||||
private bool isSelected = true;
|
||||
private T value;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的可选择的对象
|
||||
/// </summary>
|
||||
/// <param name="value">值</param>
|
||||
/// <param name="onSelectedChanged">选中的值发生变化时调用</param>
|
||||
public Selectable(T value, Action? onSelectedChanged = null)
|
||||
{
|
||||
this.value = value;
|
||||
selectedChanged = onSelectedChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 指示当前对象是否选中
|
||||
/// </summary>
|
||||
public bool IsSelected
|
||||
{
|
||||
get => isSelected;
|
||||
set
|
||||
{
|
||||
Set(ref isSelected, value);
|
||||
selectedChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 存放的对象
|
||||
/// </summary>
|
||||
public T Value { get => value; set => Set(ref this.value, value); }
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity
|
||||
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
|
||||
Publisher="CN=DGP Studio"
|
||||
Version="1.0.18.0" />
|
||||
Version="1.0.19.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>胡桃</DisplayName>
|
||||
|
||||
@@ -6,7 +6,6 @@ using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.View.Control;
|
||||
|
||||
@@ -31,8 +30,8 @@ public sealed partial class DescParamComboBox : UserControl
|
||||
/// </summary>
|
||||
public IList<LevelParam<string, ParameterInfo>> Source
|
||||
{
|
||||
get { return (IList<LevelParam<string, ParameterInfo>>)GetValue(SourceProperty); }
|
||||
set { SetValue(SourceProperty, value); }
|
||||
get => (IList<LevelParam<string, ParameterInfo>>)GetValue(SourceProperty);
|
||||
set => SetValue(SourceProperty, value);
|
||||
}
|
||||
|
||||
private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
|
||||
@@ -45,15 +44,13 @@ public sealed partial class DescParamComboBox : UserControl
|
||||
{
|
||||
descParamComboBox.ItemHost.ItemsSource = list;
|
||||
descParamComboBox.ItemHost.SelectedIndex = 0;
|
||||
|
||||
descParamComboBox.DetailsHost.ItemsSource = list.FirstOrDefault()?.Parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ItemHostSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (sender is ComboBox comboBox && comboBox.SelectedIndex >= 0)
|
||||
if (sender is ComboBox { SelectedIndex: >= 0 } comboBox)
|
||||
{
|
||||
DetailsHost.ItemsSource = Source[comboBox.SelectedIndex]?.Parameters;
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ public sealed partial class SkillPivot : UserControl
|
||||
/// </summary>
|
||||
public IList Skills
|
||||
{
|
||||
get { return (IList)GetValue(SkillsProperty); }
|
||||
set { SetValue(SkillsProperty, value); }
|
||||
get => (IList)GetValue(SkillsProperty);
|
||||
set => SetValue(SkillsProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -39,8 +39,8 @@ public sealed partial class SkillPivot : UserControl
|
||||
/// </summary>
|
||||
public object Selected
|
||||
{
|
||||
get { return GetValue(SelectedProperty); }
|
||||
set { SetValue(SelectedProperty, value); }
|
||||
get => GetValue(SelectedProperty);
|
||||
set => SetValue(SelectedProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -48,7 +48,7 @@ public sealed partial class SkillPivot : UserControl
|
||||
/// </summary>
|
||||
public DataTemplate ItemTemplate
|
||||
{
|
||||
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
|
||||
set { SetValue(ItemTemplateProperty, value); }
|
||||
get => (DataTemplate)GetValue(ItemTemplateProperty);
|
||||
set => SetValue(ItemTemplateProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
<StackPanel Margin="32,0,24,0">
|
||||
|
||||
<sc:SettingsGroup Header="关于 胡桃">
|
||||
<sc:Setting Header="胡桃" Description="{Binding AppVersion}"/>
|
||||
|
||||
<sc:SettingExpander>
|
||||
<sc:SettingExpander.Header>
|
||||
<sc:Setting
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<DataTemplate x:Key="PropertyDataTemplate">
|
||||
<shvc:DescParamComboBox
|
||||
HorizontalAlignment="Stretch"
|
||||
Source="{Binding Property,Converter={StaticResource PropertyDescriptor}}"/>
|
||||
Source="{Binding Converter={StaticResource PropertyDescriptor}}"/>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="TalentDataTemplate">
|
||||
@@ -80,32 +80,104 @@
|
||||
<SolidColorBrush Color="{ThemeResource CardBackgroundFillColorSecondary}"/>
|
||||
</SplitView.PaneBackground>
|
||||
<SplitView.Pane>
|
||||
<ListView
|
||||
SelectionMode="Single"
|
||||
ItemsSource="{Binding Avatars}"
|
||||
SelectedItem="{Binding Selected,Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shci:CachedImage
|
||||
Grid.Column="0"
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="0,0,12,12"
|
||||
Source="{Binding SideIcon,Converter={StaticResource AvatarSideIconConverter},Mode=OneWay}"/>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Grid.Column="1"
|
||||
Margin="12,0,0,0"
|
||||
Text="{Binding Name}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<CommandBar ClosedDisplayMode="Compact" DefaultLabelPosition="Right">
|
||||
<AppBarButton Label="筛选" Icon="Filter">
|
||||
<AppBarButton.Flyout>
|
||||
<Flyout Placement="RightEdgeAlignedTop" LightDismissOverlayMode="On">
|
||||
<cwuc:UniformGrid Columns="3" RowSpacing="16">
|
||||
<cwuc:HeaderedItemsControl
|
||||
Header="元素"
|
||||
Padding="0,12,0,0"
|
||||
ItemsSource="{Binding FilterElementInfos}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Content="{Binding Value}"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</cwuc:HeaderedItemsControl>
|
||||
|
||||
<cwuc:HeaderedItemsControl
|
||||
Header="所属"
|
||||
Padding="0,12,0,0"
|
||||
ItemsSource="{Binding FilterAssociationInfos}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Content="{Binding Value.Key}"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</cwuc:HeaderedItemsControl>
|
||||
|
||||
<cwuc:HeaderedItemsControl
|
||||
Header="武器"
|
||||
Padding="0,12,0,0"
|
||||
ItemsSource="{Binding FilterWeaponTypeInfos}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Content="{Binding Value.Key}"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</cwuc:HeaderedItemsControl>
|
||||
|
||||
<cwuc:HeaderedItemsControl
|
||||
Header="星级"
|
||||
Padding="0,12,0,0"
|
||||
ItemsSource="{Binding FilterQualityInfos}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Content="{Binding Value.Key}"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</cwuc:HeaderedItemsControl>
|
||||
|
||||
<cwuc:HeaderedItemsControl
|
||||
Header="体型"
|
||||
Padding="0,12,0,0"
|
||||
ItemsSource="{Binding FilterBodyInfos}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Content="{Binding Value.Key}"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</cwuc:HeaderedItemsControl>
|
||||
</cwuc:UniformGrid>
|
||||
</Flyout>
|
||||
</AppBarButton.Flyout>
|
||||
</AppBarButton>
|
||||
</CommandBar>
|
||||
<ListView
|
||||
Grid.Row="1"
|
||||
SelectionMode="Single"
|
||||
ItemsSource="{Binding Avatars}"
|
||||
SelectedItem="{Binding Selected,Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shci:CachedImage
|
||||
Grid.Column="0"
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="0,0,12,12"
|
||||
Source="{Binding SideIcon,Converter={StaticResource AvatarSideIconConverter},Mode=OneWay}"/>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Grid.Column="1"
|
||||
Margin="12,0,0,0"
|
||||
Text="{Binding Name}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</Grid>
|
||||
|
||||
</SplitView.Pane>
|
||||
<SplitView.Content>
|
||||
<Grid>
|
||||
@@ -255,7 +327,7 @@
|
||||
Margin="16,16,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Content="{Binding Selected,Mode=OneWay}"
|
||||
Content="{Binding Selected.Property,Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource PropertyDataTemplate}"/>
|
||||
|
||||
<TextBlock Text="天赋" Style="{StaticResource BaseTextBlockStyle}" Margin="16,32,0,0"/>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Context.FileSystem.Location;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Windows.Storage;
|
||||
using Windows.System;
|
||||
|
||||
namespace Snap.Hutao.ViewModel;
|
||||
@@ -14,12 +14,17 @@ namespace Snap.Hutao.ViewModel;
|
||||
[Injection(InjectAs.Transient)]
|
||||
internal class ExperimentalFeaturesViewModel : ObservableObject
|
||||
{
|
||||
private readonly IFileSystemLocation hutaoLocation;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的实验性功能视图模型
|
||||
/// </summary>
|
||||
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
|
||||
public ExperimentalFeaturesViewModel(IAsyncRelayCommandFactory asyncRelayCommandFactory)
|
||||
/// <param name="hutaoLocation">数据文件夹</param>
|
||||
public ExperimentalFeaturesViewModel(IAsyncRelayCommandFactory asyncRelayCommandFactory, HutaoLocation hutaoLocation)
|
||||
{
|
||||
this.hutaoLocation = hutaoLocation;
|
||||
|
||||
OpenCacheFolderCommand = asyncRelayCommandFactory.Create(OpenCacheFolderAsync);
|
||||
OpenDataFolderCommand = asyncRelayCommandFactory.Create(OpenDataFolderAsync);
|
||||
}
|
||||
@@ -36,12 +41,11 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
|
||||
|
||||
private Task OpenCacheFolderAsync(CancellationToken token)
|
||||
{
|
||||
return Launcher.LaunchFolderAsync(App.Current.AppData.TemporaryFolder).AsTask(token);
|
||||
return Launcher.LaunchFolderAsync(App.Current.CacheFolder).AsTask(token);
|
||||
}
|
||||
|
||||
private async Task OpenDataFolderAsync(CancellationToken token)
|
||||
private Task OpenDataFolderAsync(CancellationToken token)
|
||||
{
|
||||
StorageFolder folder = await KnownFolders.DocumentsLibrary.GetFolderAsync("Hutao").AsTask(token).ConfigureAwait(false);
|
||||
await Launcher.LaunchFolderAsync(folder).AsTask(token).ConfigureAwait(false);
|
||||
return Launcher.LaunchFolderPathAsync(hutaoLocation.GetPath()).AsTask(token);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,14 @@ internal class SettingViewModel : ObservableObject
|
||||
Experimental = experimental;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 版本
|
||||
/// </summary>
|
||||
public string AppVersion
|
||||
{
|
||||
get => Core.CoreEnvironment.Version.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实验性功能
|
||||
/// </summary>
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.WinUI.UI;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Model;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using System.Collections.Generic;
|
||||
@@ -17,8 +20,13 @@ namespace Snap.Hutao.ViewModel;
|
||||
internal class WikiAvatarViewModel : ObservableObject
|
||||
{
|
||||
private readonly IMetadataService metadataService;
|
||||
private readonly List<Selectable<string>> filterElementInfos;
|
||||
private readonly List<Selectable<Pair<string, string>>> filterAssociationInfos;
|
||||
private readonly List<Selectable<Pair<string, WeaponType>>> filterWeaponTypeInfos;
|
||||
private readonly List<Selectable<Pair<string, ItemQuality>>> filterQualityInfos;
|
||||
private readonly List<Selectable<Pair<string, string>>> filterBodyInfos;
|
||||
|
||||
private List<Avatar>? avatars;
|
||||
private AdvancedCollectionView? avatars;
|
||||
private Avatar? selected;
|
||||
|
||||
/// <summary>
|
||||
@@ -30,18 +38,88 @@ internal class WikiAvatarViewModel : ObservableObject
|
||||
{
|
||||
this.metadataService = metadataService;
|
||||
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
|
||||
|
||||
filterElementInfos = new()
|
||||
{
|
||||
new("火", OnFilterChanged),
|
||||
new("水", OnFilterChanged),
|
||||
new("草", OnFilterChanged),
|
||||
new("雷", OnFilterChanged),
|
||||
new("冰", OnFilterChanged),
|
||||
new("风", OnFilterChanged),
|
||||
new("岩", OnFilterChanged),
|
||||
};
|
||||
|
||||
filterAssociationInfos = new()
|
||||
{
|
||||
new(new("蒙德", "ASSOC_TYPE_MONDSTADT"), OnFilterChanged),
|
||||
new(new("璃月", "ASSOC_TYPE_LIYUE"), OnFilterChanged),
|
||||
new(new("稻妻", "ASSOC_TYPE_INAZUMA"), OnFilterChanged),
|
||||
new(new("愚人众", "ASSOC_TYPE_FATUI"), OnFilterChanged),
|
||||
new(new("游侠", "ASSOC_TYPE_RANGER"), OnFilterChanged),
|
||||
};
|
||||
|
||||
filterWeaponTypeInfos = new()
|
||||
{
|
||||
new(new("单手剑", WeaponType.WEAPON_SWORD_ONE_HAND), OnFilterChanged),
|
||||
new(new("法器", WeaponType.WEAPON_CATALYST), OnFilterChanged),
|
||||
new(new("双手剑", WeaponType.WEAPON_CLAYMORE), OnFilterChanged),
|
||||
new(new("弓", WeaponType.WEAPON_BOW), OnFilterChanged),
|
||||
new(new("长柄武器", WeaponType.WEAPON_POLE), OnFilterChanged),
|
||||
};
|
||||
|
||||
filterQualityInfos = new()
|
||||
{
|
||||
new(new("限定五星", ItemQuality.QUALITY_ORANGE_SP), OnFilterChanged),
|
||||
new(new("五星", ItemQuality.QUALITY_ORANGE), OnFilterChanged),
|
||||
new(new("四星", ItemQuality.QUALITY_PURPLE), OnFilterChanged),
|
||||
};
|
||||
|
||||
filterBodyInfos = new()
|
||||
{
|
||||
new(new("成女", "BODY_LADY"), OnFilterChanged),
|
||||
new(new("少女", "BODY_GIRL"), OnFilterChanged),
|
||||
new(new("幼女", "BODY_LOLI"), OnFilterChanged),
|
||||
new(new("成男", "BODY_MALE"), OnFilterChanged),
|
||||
new(new("少男", "BODY_BOY"), OnFilterChanged),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 角色列表
|
||||
/// </summary>
|
||||
public List<Avatar>? Avatars { get => avatars; set => SetProperty(ref avatars, value); }
|
||||
public AdvancedCollectionView? Avatars { get => avatars; set => SetProperty(ref avatars, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 选中的角色
|
||||
/// </summary>
|
||||
public Avatar? Selected { get => selected; set => SetProperty(ref selected, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 筛选用元素信息集合
|
||||
/// </summary>
|
||||
public IList<Selectable<string>> FilterElementInfos => filterElementInfos;
|
||||
|
||||
/// <summary>
|
||||
/// 筛选用所属国家集合
|
||||
/// </summary>
|
||||
public IList<Selectable<Pair<string, string>>> FilterAssociationInfos => filterAssociationInfos;
|
||||
|
||||
/// <summary>
|
||||
/// 筛选用武器信息集合
|
||||
/// </summary>
|
||||
public IList<Selectable<Pair<string, WeaponType>>> FilterWeaponTypeInfos => filterWeaponTypeInfos;
|
||||
|
||||
/// <summary>
|
||||
/// 筛选用星级信息集合
|
||||
/// </summary>
|
||||
public IList<Selectable<Pair<string, ItemQuality>>> FilterQualityInfos => filterQualityInfos;
|
||||
|
||||
/// <summary>
|
||||
/// 筛选用体型信息集合
|
||||
/// </summary>
|
||||
public IList<Selectable<Pair<string, string>>> FilterBodyInfos => filterBodyInfos;
|
||||
|
||||
/// <summary>
|
||||
/// 打开页面命令
|
||||
/// </summary>
|
||||
@@ -56,8 +134,48 @@ internal class WikiAvatarViewModel : ObservableObject
|
||||
.OrderBy(avatar => avatar.BeginTime)
|
||||
.ThenBy(avatar => avatar.Sort);
|
||||
|
||||
Avatars = new List<Avatar>(sorted);
|
||||
Selected = Avatars[0];
|
||||
Avatars = new AdvancedCollectionView(new List<Avatar>(sorted), true);
|
||||
Avatars.MoveCurrentToFirst();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFilterChanged()
|
||||
{
|
||||
if (Avatars is not null)
|
||||
{
|
||||
List<string> targetElements = filterElementInfos
|
||||
.Where(e => e.IsSelected)
|
||||
.Select(e => e.Value)
|
||||
.ToList();
|
||||
|
||||
List<string> targetAssociations = filterAssociationInfos
|
||||
.Where(e => e.IsSelected)
|
||||
.Select(e => e.Value.Value)
|
||||
.ToList();
|
||||
|
||||
List<WeaponType> targetWeaponTypes = filterWeaponTypeInfos
|
||||
.Where(e => e.IsSelected)
|
||||
.Select(e => e.Value.Value)
|
||||
.ToList();
|
||||
|
||||
List<ItemQuality> targeQualities = FilterQualityInfos
|
||||
.Where(e => e.IsSelected)
|
||||
.Select(e => e.Value.Value)
|
||||
.ToList();
|
||||
|
||||
List<string> targetBodies = filterBodyInfos
|
||||
.Where(e => e.IsSelected)
|
||||
.Select(e => e.Value.Value)
|
||||
.ToList();
|
||||
|
||||
Avatars.Filter = (object o) => o is Avatar avatar
|
||||
&& targetElements.Contains(avatar.FetterInfo.VisionBefore)
|
||||
&& targetAssociations.Contains(avatar.FetterInfo.Association)
|
||||
&& targetWeaponTypes.Contains(avatar.Weapon)
|
||||
&& targeQualities.Contains(avatar.Quality)
|
||||
&& targetBodies.Contains(avatar.Body);
|
||||
|
||||
Avatars.MoveCurrentToFirst();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user