handle com exception caused by LoadedImageSurface

This commit is contained in:
DismissedLight
2022-07-29 13:17:42 +08:00
parent a6ad24d534
commit 41d99c227c
28 changed files with 512 additions and 133 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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))
{

View File

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

View File

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

View File

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

View File

@@ -22,4 +22,4 @@ public static class TupleExtensions
{
return new Dictionary<TKey, TValue>(1) { { tuple.Key, tuple.Value } };
}
}
}

View File

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

View File

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

View 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,
}

View File

@@ -43,4 +43,4 @@ public enum EquipType
/// 武器
/// </summary>
EQUIP_WEAPON = 6,
}
}

View File

@@ -29,7 +29,10 @@ public class SkillDepot
/// <summary>
/// 全部天赋
/// </summary>
public IList<ProudableSkill> CompositeSkills => GetCompositeSkills().ToList();
public IList<ProudableSkill> CompositeSkills
{
get => GetCompositeSkills().ToList();
}
/// <summary>
/// 命之座

View File

@@ -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/>

View 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; }
}

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

View File

@@ -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>

View File

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

View File

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

View File

@@ -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

View File

@@ -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"/>

View File

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

View File

@@ -20,6 +20,14 @@ internal class SettingViewModel : ObservableObject
Experimental = experimental;
}
/// <summary>
/// 版本
/// </summary>
public string AppVersion
{
get => Core.CoreEnvironment.Version.ToString();
}
/// <summary>
/// 实验性功能
/// </summary>

View File

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