viewmodels, uis, ecc.
This commit is contained in:
parent
bda455b7e3
commit
1d706b1adc
@ -12,6 +12,9 @@ namespace HashCalculator.GUI
|
||||
public IEnumerable<string> DisplayLabels { get; }
|
||||
public uint Hash => SageHash.CalculateLowercaseHash(Name);
|
||||
|
||||
public string IdString => $"{Type}:{Name}";
|
||||
public string HashString => $"{Hash:0X} ({Hash})";
|
||||
|
||||
public AssetEntry(XElement element)
|
||||
{
|
||||
if (element == null)
|
||||
@ -26,12 +29,7 @@ namespace HashCalculator.GUI
|
||||
|
||||
Type = element.Name.LocalName;
|
||||
var id = element.Attribute("id")?.Value;
|
||||
if (id == null)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
Name = id;
|
||||
Name = id ?? throw new NotSupportedException();
|
||||
|
||||
var labels = from name in element.Elements(ModXml.EalaAsset + "DisplayName")
|
||||
select name.Value;
|
||||
|
131
Command.cs
Normal file
131
Command.cs
Normal file
@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace HashCalculator.GUI
|
||||
{
|
||||
internal class Command<T> : ICommand
|
||||
{
|
||||
private readonly Func<T, Task> _action;
|
||||
private readonly Action<Exception> _onAsyncException;
|
||||
private bool _canExecute = true;
|
||||
|
||||
public bool CanExecuteValue
|
||||
{
|
||||
get => _canExecute;
|
||||
set
|
||||
{
|
||||
_canExecute = value;
|
||||
CanExecuteChanged?.Invoke(this, new EventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public Command(Func<T, Task> action, Action<Exception>? onAsyncException = null)
|
||||
{
|
||||
_action = action;
|
||||
_onAsyncException = onAsyncException ?? (exception =>
|
||||
{
|
||||
MessageBox.Show($"Unhandled async exception in {nameof(Command<T>)}: {exception}");
|
||||
Application.Current.Shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
public bool CanExecute(object parameter)
|
||||
{
|
||||
return _canExecute;
|
||||
}
|
||||
|
||||
[SuppressMessage("Globalization", "CA1303:请不要将文本作为本地化参数传递", Justification = "<挂起>")]
|
||||
public void Execute(object parameter)
|
||||
{
|
||||
if (!_canExecute)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(!(parameter is T typed))
|
||||
{
|
||||
throw new ArgumentException($"{nameof(parameter)} wrong type");
|
||||
}
|
||||
|
||||
ExecuteTaskInternal(ExecuteTask(typed));
|
||||
}
|
||||
|
||||
public Task ExecuteTask(T parameter) => _action(parameter);
|
||||
|
||||
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
|
||||
private async void ExecuteTaskInternal(Task task)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task.ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_onAsyncException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class Command : ICommand
|
||||
{
|
||||
private readonly Func<Task> _action;
|
||||
private readonly Action<Exception> _onAsyncException;
|
||||
private bool _canExecute = true;
|
||||
|
||||
public bool CanExecuteValue
|
||||
{
|
||||
get => _canExecute;
|
||||
set
|
||||
{
|
||||
_canExecute = value;
|
||||
CanExecuteChanged?.Invoke(this, new EventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public Command(Func<Task> action, Action<Exception>? onAsyncException = null)
|
||||
{
|
||||
_action = action;
|
||||
_onAsyncException = onAsyncException ?? (exception =>
|
||||
{
|
||||
MessageBox.Show($"Unhandled async exception in {nameof(Command)}: {exception}");
|
||||
Application.Current.Shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
public bool CanExecute(object parameter)
|
||||
{
|
||||
return _canExecute;
|
||||
}
|
||||
|
||||
public void Execute(object parameter)
|
||||
{
|
||||
if (!_canExecute)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ExecuteTaskInternal(ExecuteTask());
|
||||
}
|
||||
|
||||
public Task ExecuteTask() => _action();
|
||||
|
||||
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
|
||||
private async void ExecuteTaskInternal(Task task)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task.ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_onAsyncException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
Converters/BooleanInvertConverter.cs
Normal file
22
Converters/BooleanInvertConverter.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace HashCalculator.GUI.Converters
|
||||
{
|
||||
[ValueConversion(typeof(bool), typeof(bool))]
|
||||
internal class BooleanInvertConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
bool original = (bool)value;
|
||||
return !original;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
bool original = (bool)value;
|
||||
return !original;
|
||||
}
|
||||
}
|
||||
}
|
28
Converters/NullToBooleanConverter.cs
Normal file
28
Converters/NullToBooleanConverter.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace HashCalculator.GUI.Converters
|
||||
{
|
||||
[ValueConversion(typeof(object), typeof(bool))]
|
||||
public class NullToBooleanConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if(value is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if(value is string s)
|
||||
{
|
||||
return !string.IsNullOrEmpty(s);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
21
Converters/ValidInputEntryTypeToBooleanConverter.cs
Normal file
21
Converters/ValidInputEntryTypeToBooleanConverter.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace HashCalculator.GUI.Converters
|
||||
{
|
||||
[ValueConversion(typeof(InputEntry), typeof(bool))]
|
||||
internal class ValidInputEntryTypeToBooleanConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var entry = value as InputEntry;
|
||||
return entry?.IsValid == true;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Linq;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace HashCalculator.GUI
|
||||
namespace HashCalculator.GUI.Converters
|
||||
{
|
||||
[ValueConversion(typeof(string), typeof(Visibility))]
|
||||
public class NullToVisibilityConverter : IValueConverter
|
||||
internal class ValueConverterAggregate : List<IValueConverter>, IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var text = value as string;
|
||||
return string.IsNullOrEmpty(text) ? Visibility.Visible : Visibility.Collapsed;
|
||||
return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture));
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
50
CopyableBox.xaml
Normal file
50
CopyableBox.xaml
Normal file
@ -0,0 +1,50 @@
|
||||
<Window
|
||||
x:Class="HashCalculator.GUI.CopyableBox"
|
||||
x:Name="Self"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:HashCalculator.GUI"
|
||||
mc:Ignorable="d"
|
||||
Title="CopyableBox" Height="350" Width="600"
|
||||
Closing="ClosingHandler"
|
||||
>
|
||||
<Window.DataContext>
|
||||
<local:CopyableBoxViewModel />
|
||||
</Window.DataContext>
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="60"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Button
|
||||
Content="复制所有内容"
|
||||
Command="{Binding CopyCommand}"
|
||||
Grid.Row="1"
|
||||
Margin="10,0,0,10"
|
||||
Width="120"
|
||||
Height="30"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Bottom"
|
||||
/>
|
||||
<Button
|
||||
Content="{Binding CloseButtonText}"
|
||||
Command="{Binding CloseCommand}"
|
||||
CommandParameter="{Binding ElementName=Self}"
|
||||
Margin="0,0,10,10"
|
||||
Grid.Row="1"
|
||||
Width="100"
|
||||
Height="30"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
/>
|
||||
<TextBox
|
||||
Grid.Row="0"
|
||||
Text="{Binding Text, Mode=OneWay}"
|
||||
IsReadOnly="True"
|
||||
TextWrapping="Wrap"
|
||||
/>
|
||||
</Grid>
|
||||
</Window>
|
166
CopyableBox.xaml.cs
Normal file
166
CopyableBox.xaml.cs
Normal file
@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Threading;
|
||||
using TechnologyAssembler.Core.Extensions;
|
||||
|
||||
namespace HashCalculator.GUI
|
||||
{
|
||||
/// <summary>
|
||||
/// CopyableBox.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
[SuppressMessage("Microsoft.Performance", "CA1812")]
|
||||
internal partial class CopyableBox : Window, IDisposable
|
||||
{
|
||||
private CopyableBoxViewModel ViewModel => (CopyableBoxViewModel)DataContext;
|
||||
|
||||
public static void ShowDialog(Func<CancellationToken, Task<string>> action)
|
||||
{
|
||||
using var box = new CopyableBox();
|
||||
box.ViewModel.Initialize(action, box.Dispatcher);
|
||||
box.ShowDialog();
|
||||
}
|
||||
|
||||
private CopyableBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ViewModel.Dispose();
|
||||
}
|
||||
|
||||
private void ClosingHandler(object sender, CancelEventArgs e)
|
||||
{
|
||||
if (ViewModel.ReadyToClose)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.Cancel = true;
|
||||
if (ViewModel.CloseCommand.CanExecuteValue)
|
||||
{
|
||||
ViewModel.CloseCommand.Execute(this);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1812")]
|
||||
internal class CopyableBoxViewModel : NotifyPropertyChanged, IDisposable
|
||||
{
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
private Task? _task;
|
||||
|
||||
public const string InitialMessage = "正在加载…… 关闭窗口就可以取消加载(";
|
||||
|
||||
private string _text = InitialMessage;
|
||||
public string Text
|
||||
{
|
||||
get => _text;
|
||||
private set => SetField(ref _text, value);
|
||||
}
|
||||
|
||||
public string CloseButtonText => CloseCommand.CanExecuteValue ? "关闭窗口" : "正在关闭…";
|
||||
private bool _readyToClose = false;
|
||||
public bool ReadyToClose
|
||||
{
|
||||
get => _readyToClose;
|
||||
private set => SetField(ref _readyToClose, value);
|
||||
}
|
||||
|
||||
public Command CopyCommand { get; }
|
||||
public Command<CopyableBox> CloseCommand { get; }
|
||||
|
||||
public CopyableBoxViewModel()
|
||||
{
|
||||
CopyCommand = new Command(() =>
|
||||
{
|
||||
Clipboard.SetText(Text);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
CloseCommand = new Command<CopyableBox>(async window =>
|
||||
{
|
||||
CloseCommand.CanExecuteValue = false;
|
||||
Notify(nameof(CloseButtonText));
|
||||
|
||||
if (_task != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Cancel();
|
||||
await _task.ConfigureAwait(true);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
ReadyToClose = true;
|
||||
window.Close();
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Cancel();
|
||||
_cancellationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
public void Initialize(Func<CancellationToken, Task<string>> action, Dispatcher dispatcher)
|
||||
{
|
||||
_task = InitializationTask(action, dispatcher);
|
||||
}
|
||||
|
||||
private async Task InitializationTask(Func<CancellationToken, Task<string>> action, Dispatcher dispatcher)
|
||||
{
|
||||
var utcBegin = DateTimeOffset.UtcNow;
|
||||
var timer = new DispatcherTimer(TimeSpan.FromMilliseconds(200), DispatcherPriority.Normal, (s, e) =>
|
||||
{
|
||||
var timeElapsed = DateTimeOffset.UtcNow - utcBegin;
|
||||
if (((DispatcherTimer)s).IsEnabled && timeElapsed.TotalSeconds > 1)
|
||||
{
|
||||
Text = Text = $"{InitialMessage}\r\n目前耗时{timeElapsed},稍微再等一下吧233";
|
||||
}
|
||||
}, dispatcher);
|
||||
timer.Start();
|
||||
|
||||
var token = _cancellationTokenSource.Token;
|
||||
async Task<string> Action()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await action(token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return "操作已被取消";
|
||||
}
|
||||
finally
|
||||
{
|
||||
timer.Stop();
|
||||
}
|
||||
}
|
||||
Text = await dispatcher.Invoke(Action).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,11 @@ namespace HashCalculator.GUI
|
||||
|
||||
path = Environment.ExpandEnvironmentVariables(path);
|
||||
|
||||
if (path.IndexOfAny(Path.GetInvalidPathChars()) != -1)
|
||||
{
|
||||
return empty;
|
||||
}
|
||||
|
||||
_search.Update(path);
|
||||
|
||||
if (!_search.IsValidPath)
|
||||
@ -37,7 +42,7 @@ namespace HashCalculator.GUI
|
||||
return empty;
|
||||
}
|
||||
|
||||
var currentFiles = empty;
|
||||
var currentFiles = new List<InputEntry>();
|
||||
string? fileName;
|
||||
string? currentFullPath;
|
||||
try
|
||||
@ -46,13 +51,12 @@ namespace HashCalculator.GUI
|
||||
fileName = Path.GetFileName(path);
|
||||
if (File.Exists(currentFullPath))
|
||||
{
|
||||
|
||||
var type = CheckExtension(currentFullPath);
|
||||
if (type.HasValue)
|
||||
if(type is InputEntryType entryType)
|
||||
{
|
||||
currentFiles.Append(new InputEntry(type.Value, path, currentFullPath));
|
||||
currentFiles.Add(new InputEntry(entryType, path, currentFullPath));
|
||||
}
|
||||
currentFiles.Append(new InputEntry(InputEntryType.BinaryFile, path, currentFullPath));
|
||||
currentFiles.Add(new InputEntry(InputEntryType.BinaryFile, path, currentFullPath));
|
||||
}
|
||||
}
|
||||
catch
|
||||
@ -60,34 +64,31 @@ namespace HashCalculator.GUI
|
||||
return empty;
|
||||
}
|
||||
|
||||
var directories = from directory in _search.AllDirectories
|
||||
where directory.Name.StartsWith(fileName, StringComparison.OrdinalIgnoreCase)
|
||||
select new InputEntry(InputEntryType.Path, _search.GetInputStyleName(directory), directory.FullName);
|
||||
|
||||
var otherFiles = from file in _search.AllFiles
|
||||
where file.Name.StartsWith(fileName, StringComparison.OrdinalIgnoreCase)
|
||||
where file.FullName != currentFullPath
|
||||
select file;
|
||||
|
||||
var supportedFiles = empty;
|
||||
var alternatives = empty;
|
||||
try
|
||||
{
|
||||
supportedFiles = from file in otherFiles
|
||||
let type = CheckExtension(file.Extension)
|
||||
where type.HasValue
|
||||
select new InputEntry(type.Value, _search.GetInputStyleName(file), file.FullName);
|
||||
var directories = from directory in _search.AllDirectories
|
||||
where directory.Name.StartsWith(fileName, StringComparison.OrdinalIgnoreCase)
|
||||
select new InputEntry(InputEntryType.Path, _search.GetInputStyleName(directory), directory.FullName);
|
||||
|
||||
var otherFiles = from file in _search.AllFiles
|
||||
where file.Name.StartsWith(fileName, StringComparison.OrdinalIgnoreCase)
|
||||
where file.FullName != currentFullPath
|
||||
select file;
|
||||
|
||||
var supportedFiles = from file in otherFiles
|
||||
let type = CheckExtension(file.Extension)
|
||||
where type.HasValue
|
||||
select new InputEntry(type.Value, _search.GetInputStyleName(file), file.FullName);
|
||||
|
||||
var binaryFiles = from file in otherFiles
|
||||
select new InputEntry(InputEntryType.BinaryFile, _search.GetInputStyleName(file), file.FullName);
|
||||
|
||||
alternatives = supportedFiles.Concat(directories).Concat(binaryFiles).ToArray();
|
||||
}
|
||||
catch { }
|
||||
|
||||
var binaryFiles = empty;
|
||||
try
|
||||
{
|
||||
binaryFiles = from file in otherFiles
|
||||
select new InputEntry(InputEntryType.BinaryFile, _search.GetInputStyleName(file), file.FullName);
|
||||
}
|
||||
catch { }
|
||||
|
||||
return currentFiles.Concat(supportedFiles).Concat(directories).Concat(binaryFiles);
|
||||
return currentFiles.Concat(alternatives);
|
||||
}
|
||||
|
||||
private static InputEntryType? CheckExtension(string path)
|
||||
@ -130,6 +131,12 @@ namespace HashCalculator.GUI
|
||||
string? newBaseDirectory = null;
|
||||
try
|
||||
{
|
||||
if (Path.GetFileName(path).IndexOfAny(Path.GetInvalidFileNameChars()) != -1)
|
||||
{
|
||||
_inputBaseDirectory = null;
|
||||
return;
|
||||
}
|
||||
|
||||
newBaseDirectory = Path.GetDirectoryName(path);
|
||||
var rootDirectory = Path.GetPathRoot(path);
|
||||
if(string.IsNullOrEmpty(newBaseDirectory) && !string.IsNullOrEmpty(rootDirectory))
|
||||
|
@ -21,6 +21,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Windows.Presentation" />
|
||||
<Reference Include="TechnologyAssembler.Core">
|
||||
<HintPath>TechnologyAssembler.Core.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
|
@ -5,13 +5,18 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:HashCalculator.GUI"
|
||||
xmlns:c="clr-namespace:HashCalculator.GUI.Converters"
|
||||
mc:Ignorable="d"
|
||||
x:ClassModifier="internal"
|
||||
x:Name="_this"
|
||||
d:DesignHeight="100" d:DesignWidth="800"
|
||||
>
|
||||
<UserControl.Resources>
|
||||
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||
<c:ValueConverterAggregate x:Key="NullToVisibilityConverter">
|
||||
<c:NullToBooleanConverter />
|
||||
<c:BooleanInvertConverter />
|
||||
<BooleanToVisibilityConverter />
|
||||
</c:ValueConverterAggregate>
|
||||
<Style
|
||||
x:Key="BlankListBoxContainerStyle"
|
||||
TargetType="{x:Type ListBoxItem}"
|
||||
@ -72,7 +77,6 @@
|
||||
PreviewKeyDown="OnPreviewKeyDown"
|
||||
Padding="2, 0"
|
||||
VerticalContentAlignment="Center"
|
||||
TextChanged="OnTextChanged"
|
||||
LostFocus="OnLostFocus"
|
||||
/>
|
||||
<TextBlock
|
||||
@ -98,14 +102,28 @@
|
||||
RelativeSource={RelativeSource AncestorType=local:InputBar}}"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="OnSelectionChanged"
|
||||
SelectedItem="{Binding Path=SelectedItem,
|
||||
RelativeSource={RelativeSource AncestorType=local:InputBar}}"
|
||||
SelectedIndex="{Binding Path=SelectedIndex,
|
||||
RelativeSource={RelativeSource AncestorType=local:InputBar}}"
|
||||
VirtualizingPanel.IsVirtualizing="True"
|
||||
VirtualizingPanel.VirtualizationMode="Recycling"
|
||||
>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Text="{Binding Text}" FontWeight="Bold"></TextBlock>
|
||||
<TextBlock Text="{Binding Type}"></TextBlock>
|
||||
<TextBlock Text="{Binding Text}"></TextBlock>
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
>
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="Gray" />
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<TextBlock Text="{Binding Type}"></TextBlock>
|
||||
<TextBlock Text="{Binding Details}" Margin="10,0,0,0"></TextBlock>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
|
@ -82,7 +82,22 @@ namespace HashCalculator.GUI
|
||||
set => SetValue(SelectedItemProperty, value);
|
||||
}
|
||||
|
||||
public event TextChangedEventHandler? TextChanged;
|
||||
public static readonly DependencyProperty SelectedIndexProperty =
|
||||
DependencyProperty.Register(nameof(SelectedIndex), typeof(int), typeof(InputBar), new FrameworkPropertyMetadata
|
||||
{
|
||||
BindsTwoWayByDefault = true,
|
||||
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
|
||||
CoerceValueCallback = (d, baseObject) =>
|
||||
{
|
||||
var self = (InputBar)d;
|
||||
return self.NormalizeIndex((int)baseObject);
|
||||
}
|
||||
});
|
||||
public int SelectedIndex
|
||||
{
|
||||
get => (int)GetValue(SelectedIndexProperty);
|
||||
set => SetValue(SelectedIndexProperty, value);
|
||||
}
|
||||
|
||||
public InputBar()
|
||||
{
|
||||
@ -94,12 +109,12 @@ namespace HashCalculator.GUI
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Down:
|
||||
ListBox.SelectedIndex = NormalizeIndex(ListBox.SelectedIndex + 1);
|
||||
SelectedIndex += 1;
|
||||
ListBox.ScrollIntoView(ListBox.SelectedItem);
|
||||
DropDown.IsOpen = true;
|
||||
break;
|
||||
case Key.Up:
|
||||
ListBox.SelectedIndex = NormalizeIndex(ListBox.SelectedIndex - 1);
|
||||
SelectedIndex -= 1;
|
||||
ListBox.ScrollIntoView(ListBox.SelectedItem);
|
||||
break;
|
||||
case Key.Escape:
|
||||
@ -114,7 +129,6 @@ namespace HashCalculator.GUI
|
||||
|
||||
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
SelectedItem = (InputEntry)ListBox.SelectedItem;
|
||||
if (SelectedItem != null)
|
||||
{
|
||||
Text = SelectedItem.ToString();
|
||||
@ -122,14 +136,6 @@ namespace HashCalculator.GUI
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (Text != SelectedItem?.ToString())
|
||||
{
|
||||
TextChanged?.Invoke(this, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLostFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DropDown.IsOpen = false;
|
||||
@ -145,6 +151,5 @@ namespace HashCalculator.GUI
|
||||
return Math.Min(Math.Max(rawIndex, -1), Collection.Count() - 1);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -19,9 +19,34 @@ namespace HashCalculator.GUI
|
||||
|
||||
internal sealed class InputEntry
|
||||
{
|
||||
public static IReadOnlyDictionary<InputEntryType, string> Descriptions = new Dictionary<InputEntryType, string>
|
||||
{
|
||||
{ InputEntryType.BigFile, "可以尝试读取这个 big 文件里的 manifest 文件" },
|
||||
{ InputEntryType.BinaryFile, "可以计算这个文件的哈希值" },
|
||||
{ InputEntryType.ManifestFile, "可以尝试读取这个 manifest 文件,显示各个素材的哈希值" },
|
||||
{ InputEntryType.XmlFile, "可以尝试读取这个XML,显示 XML 里定义的各个素材的哈希值" },
|
||||
{ InputEntryType.Path, string.Empty },
|
||||
};
|
||||
|
||||
public InputEntryType Type { get; }
|
||||
public string Value { get; }
|
||||
public string Text { get; }
|
||||
public string Details
|
||||
{
|
||||
get
|
||||
{
|
||||
if(Type == InputEntryType.Text)
|
||||
{
|
||||
var hash = SageHash.CalculateLowercaseHash(Value);
|
||||
var binaryHash = SageHash.CalculateBinaryHash(Value);
|
||||
return hash == binaryHash
|
||||
? $"这段文字的哈希值:{hash:x8} ({hash})"
|
||||
: $"这段文字的哈希值:{hash:x8};大小写敏感哈希值 {binaryHash:x8}";
|
||||
}
|
||||
return Descriptions[Type];
|
||||
}
|
||||
}
|
||||
public bool IsValid => Type != InputEntryType.Text && Type != InputEntryType.Path;
|
||||
|
||||
public InputEntry(InputEntryType type, string text, string value)
|
||||
{
|
||||
@ -34,6 +59,5 @@ namespace HashCalculator.GUI
|
||||
{
|
||||
return Text;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
233
MainWindow.xaml
233
MainWindow.xaml
@ -1,71 +1,228 @@
|
||||
<Window x:Class="HashCalculator.GUI.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:l="clr-namespace:HashCalculator.GUI"
|
||||
mc:Ignorable="d"
|
||||
Title="Sage FastHash 哈希计算器" Height="600" Width="600">
|
||||
<Window
|
||||
x:Class="HashCalculator.GUI.MainWindow"
|
||||
x:Name="Self"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:l="clr-namespace:HashCalculator.GUI"
|
||||
xmlns:c="clr-namespace:HashCalculator.GUI.Converters"
|
||||
mc:Ignorable="d"
|
||||
Title="Sage FastHash 哈希计算器" Height="600" Width="600"
|
||||
Background="#202020"
|
||||
Foreground="LightGray"
|
||||
>
|
||||
<Window.Resources>
|
||||
<l:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||
<c:ValueConverterAggregate x:Key="ValidInputEntryToVisibilityConverter">
|
||||
<c:ValidInputEntryTypeToBooleanConverter />
|
||||
<BooleanToVisibilityConverter />
|
||||
</c:ValueConverterAggregate>
|
||||
<c:ValueConverterAggregate x:Key="InvalidInputEntryToVisibilityConverter">
|
||||
<c:ValidInputEntryTypeToBooleanConverter />
|
||||
<c:BooleanInvertConverter />
|
||||
<BooleanToVisibilityConverter />
|
||||
</c:ValueConverterAggregate>
|
||||
<Style x:Key="CommonStyle" TargetType="Control">
|
||||
<Setter
|
||||
Property="Foreground"
|
||||
Value="{Binding Path=(TextElement.Foreground),
|
||||
RelativeSource={RelativeSource AncestorType={x:Type FrameworkElement}}}"
|
||||
/>
|
||||
<Setter
|
||||
Property="Background"
|
||||
Value="{Binding Path=(TextElement.Background),
|
||||
RelativeSource={RelativeSource AncestorType={x:Type FrameworkElement}}}"
|
||||
/>
|
||||
</Style>
|
||||
<Style
|
||||
BasedOn="{StaticResource CommonStyle}"
|
||||
TargetType="{x:Type TextBox}"
|
||||
>
|
||||
<Setter Property="Background" Value="#00000000"/>
|
||||
</Style>
|
||||
<Style
|
||||
BasedOn="{StaticResource CommonStyle}"
|
||||
TargetType="{x:Type Button}"
|
||||
>
|
||||
<Setter Property="Background" Value="#20808080"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type Button}">
|
||||
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
|
||||
<ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="Button.IsDefaulted" Value="True">
|
||||
<Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" TargetName="border" Value="#80BEE6FD"/>
|
||||
<Setter Property="BorderBrush" TargetName="border" Value="#FF3C7FB1"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" TargetName="border" Value="#80C4E5F6"/>
|
||||
<Setter Property="BorderBrush" TargetName="border" Value="#FF2C628B"/>
|
||||
</Trigger>
|
||||
<Trigger Property="ToggleButton.IsChecked" Value="True">
|
||||
<Setter Property="Background" TargetName="border" Value="#FFBCDDEE"/>
|
||||
<Setter Property="BorderBrush" TargetName="border" Value="#FF245A83"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Background" TargetName="border" Value="#00000000"/>
|
||||
<Setter Property="BorderBrush" TargetName="border" Value="#FFADB2B5"/>
|
||||
<Setter Property="Foreground" Value="#808080"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style
|
||||
BasedOn="{StaticResource CommonStyle}"
|
||||
TargetType="{x:Type DataGrid}"
|
||||
/>
|
||||
<Style
|
||||
BasedOn="{StaticResource CommonStyle}"
|
||||
TargetType="{x:Type DataGridColumnHeader}"
|
||||
>
|
||||
<Setter Property="Padding" Value="10,5" />
|
||||
<Setter Property="MinWidth" Value="0" />
|
||||
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Background" Value="#343434"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderThickness" Value="0, 0, 1, 0"/>
|
||||
<Setter Property="BorderBrush" Value="Gray" />
|
||||
</Style>
|
||||
<Style
|
||||
BasedOn="{StaticResource CommonStyle}"
|
||||
TargetType="{x:Type ListBox}"
|
||||
>
|
||||
<Setter Property="Background" Value="#181818"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
<Grid Margin="10, 10">
|
||||
<Window.DataContext>
|
||||
<l:ViewModel/>
|
||||
</Window.DataContext>
|
||||
<Grid Margin="10,20,10,10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="130" />
|
||||
</Grid.RowDefinitions>
|
||||
<VirtualizingStackPanel Grid.Row="0">
|
||||
<Grid Height="25">
|
||||
<DockPanel Grid.Row="0">
|
||||
<Grid
|
||||
DockPanel.Dock="Top"
|
||||
Height="25"
|
||||
>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="92"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid>
|
||||
<!--<l:InputBox
|
||||
HintText="Select some big file"
|
||||
Collection="{Binding Items}"
|
||||
TextChanged="OnTextChanged"
|
||||
>
|
||||
</l:InputBox>-->
|
||||
<l:InputBar
|
||||
HintText="Select some big file"
|
||||
Text="{Binding Text}"
|
||||
Collection="{Binding Items}"
|
||||
TextChanged="OnMainInputTextChanged"
|
||||
/>
|
||||
</Grid>
|
||||
<Button x:Name="button" Content="浏览文件"
|
||||
Grid.Column="1" Margin="0"
|
||||
<l:InputBar
|
||||
HintText="输入文本,或 big/manifest/xml 文件路径"
|
||||
Text="{Binding MainInput.Text}"
|
||||
Collection="{Binding MainInput.Items}"
|
||||
SelectedItem="{Binding MainInput.SelectedItem}"
|
||||
SelectedIndex="{Binding MainInput.SelectedIndex}"
|
||||
Grid.Column="0"
|
||||
Margin="0,0,10,0"
|
||||
/>
|
||||
<Button
|
||||
Content="浏览文件"
|
||||
Command="{Binding MainInput.BrowseCommand}"
|
||||
CommandParameter="{Binding ElementName=Self}"
|
||||
Visibility="{Binding MainInput.SelectedItem, Converter={StaticResource InvalidInputEntryToVisibilityConverter}}"
|
||||
Grid.Column="1"
|
||||
Margin="0"
|
||||
/>
|
||||
<Button
|
||||
Content="确认"
|
||||
Command="{Binding MainInput.SelectCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
Visibility="{Binding MainInput.SelectedItem, Converter={StaticResource ValidInputEntryToVisibilityConverter}}"
|
||||
Grid.Column="1"
|
||||
Margin="0"
|
||||
Foreground="#20FF30"
|
||||
BorderBrush="#20FF30"
|
||||
/>
|
||||
</Grid>
|
||||
<l:InputBar
|
||||
HintText="输入big文件里包含的manifest文件的路径"
|
||||
Collection="{Binding Items}"
|
||||
SelectedItem="{Binding SelectedItem}"
|
||||
<Grid
|
||||
DockPanel.Dock="Top"
|
||||
Height="25"
|
||||
Margin="0,10,0,0"
|
||||
Height="25" VerticalAlignment="Top"
|
||||
/>
|
||||
Visibility="Visible"
|
||||
>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="92"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<l:InputBar
|
||||
HintText="输入 big 文件里包含的 manifest 文件的路径"
|
||||
Collection="{Binding Items}"
|
||||
SelectedItem="{Binding SelectedItem}"
|
||||
Grid.Column="0"
|
||||
Margin="0,0,10,0"
|
||||
/>
|
||||
<Button
|
||||
Content="确认"
|
||||
Grid.Column="1"
|
||||
Margin="0"
|
||||
Foreground="ForestGreen"
|
||||
BorderBrush="ForestGreen" Template="{DynamicResource ButtonBaseControlTemplate1}"
|
||||
/>
|
||||
</Grid>
|
||||
<DockPanel
|
||||
DockPanel.Dock="Top"
|
||||
Height="25"
|
||||
Margin="0,10"
|
||||
>
|
||||
<Button
|
||||
DockPanel.Dock="Right"
|
||||
Content="取消加载"
|
||||
Grid.Column="1"
|
||||
Width="92"
|
||||
/>
|
||||
<Button
|
||||
DockPanel.Dock="Right"
|
||||
Content="加载 csf / mod.str"
|
||||
Grid.Column="1"
|
||||
Width="140"
|
||||
Margin="10,0"
|
||||
/>
|
||||
<TextBlock
|
||||
Text="正在加载……"
|
||||
VerticalAlignment="Center"
|
||||
Grid.Column="0"
|
||||
/>
|
||||
</DockPanel>
|
||||
<l:InputBar
|
||||
HintText="过滤Asset ID(可选)"
|
||||
Collection="{Binding Items}"
|
||||
SelectedItem="{Binding SelectedItem}"
|
||||
Margin="0,10,0,0"
|
||||
Height="25" VerticalAlignment="Top"
|
||||
DockPanel.Dock="Top"
|
||||
Height="25"
|
||||
/>
|
||||
<DataGrid
|
||||
x:Name="DataGrid"
|
||||
AutoGenerateColumns="False"
|
||||
ScrollViewer.CanContentScroll="True"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
EnableRowVirtualization="True"
|
||||
VirtualizingPanel.VirtualizationMode="Recycling"
|
||||
DockPanel.Dock="Top"
|
||||
>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Asset ID" Width="200" Binding="{Binding AssetId}"/>
|
||||
<DataGridTextColumn Header="哈希" Width="*" Binding="{Binding Hash}"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</VirtualizingStackPanel>
|
||||
<TextBlock x:Name="textBlock" Grid.Row="1" TextWrapping="Wrap">
|
||||
</DockPanel>
|
||||
<TextBlock
|
||||
x:Name="textBlock"
|
||||
Grid.Row="1"
|
||||
Margin="0,10,0,0"
|
||||
TextWrapping="Wrap"
|
||||
>
|
||||
本工具基于 <l:ShellLink NavigateUri="https://github.com/Qibbi">Qibbi</l:ShellLink> 提供的 TechnologyAssembler 制作<LineBreak />
|
||||
此外使用了 <l:ShellLink NavigateUri="https://github.com/bgrainger">Bradley Grainger</l:ShellLink> 的
|
||||
<l:ShellLink NavigateUri="https://github.com/bgrainger/IndexRange">IndexRange</l:ShellLink>
|
||||
|
@ -20,59 +20,28 @@ namespace HashCalculator.GUI
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
|
||||
internal ViewModel ViewModel => (ViewModel)DataContext;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
var x = new ModXml(@"D:\Users\lanyi\Desktop\RA3Mods\ARSDK2\Mods\Armor Rush\DATA\mod.xml");
|
||||
var xxx = Enumerable.Repeat(new { AssetId = "正在准备", Hash = "正在准备" }, 1);
|
||||
x.ProcessDocument().ContinueWith(async txx =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var xx = await txx.ConfigureAwait(true);
|
||||
if (x.Errors.Any())
|
||||
{
|
||||
MessageBox.Show("Errors: \r\n" + x.Errors.Aggregate((x, y) => $"{x}\r\n{y}"));
|
||||
}
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
DataGrid.ItemsSource = from y in xx
|
||||
select new { AssetId = $"{y.Type}:{y.Name}", y.Hash };
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
DataGrid.ItemsSource = Enumerable.Repeat(new { AssetId = $"错误 {e}", Hash = e.ToString() }, 1);
|
||||
});
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
|
||||
/*_ = Task.Run(async () =>
|
||||
{
|
||||
var xxx = Enumerable.Repeat(new { AssetId = "正在准备", Hash = "正在准备" }, 1);
|
||||
await Dispatcher.Invoke(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var xx = await x.ProcessDocument().ConfigureAwait(true);
|
||||
DataGrid.ItemsSource = from y in xx
|
||||
select new { AssetId = $"{y.Type}:{y.Name}", y.Hash };
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DataGrid.ItemsSource = Enumerable.Repeat(new { AssetId = $"错误 {e}", Hash = e.ToString() }, 1);
|
||||
}
|
||||
|
||||
}).ConfigureAwait(true);
|
||||
});*/
|
||||
}
|
||||
|
||||
private void OnMainInputTextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
CopyableBox.ShowDialog(async token =>
|
||||
{
|
||||
var init = DateTimeOffset.UtcNow;
|
||||
var i = 0;
|
||||
while (i < 100)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
++i;
|
||||
}
|
||||
MessageBox.Show("Completed!");
|
||||
return $"Completed after exactly {DateTimeOffset.UtcNow - init}";
|
||||
});
|
||||
/*var mainInput = _viewModel.MainInput;
|
||||
if (sender is InputBar)
|
||||
{
|
||||
|
19
ModManifest.cs
Normal file
19
ModManifest.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TechnologyAssembler.Core.Assets;
|
||||
|
||||
namespace HashCalculator.GUI
|
||||
{
|
||||
public class ModManifest
|
||||
{
|
||||
IReadOnlyCollection<AssetEntry> Entries;
|
||||
|
||||
public ModManifest()
|
||||
{
|
||||
var x = Manifest.Load("");
|
||||
}
|
||||
}
|
||||
}
|
33
Model.cs
33
Model.cs
@ -1,33 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace HashCalculator.GUI
|
||||
{
|
||||
internal class Model
|
||||
{
|
||||
private readonly ViewModel _viewModel;
|
||||
private readonly FileSystemSuggestions _suggestions = new FileSystemSuggestions();
|
||||
|
||||
public Model(ViewModel viewModel)
|
||||
{
|
||||
_viewModel = viewModel;
|
||||
}
|
||||
|
||||
public void UpdateMainInputSuggestions()
|
||||
{
|
||||
var mainInput = _viewModel.MainInput;
|
||||
var inputValue = mainInput.Text;
|
||||
if (inputValue != null)
|
||||
{
|
||||
mainInput.Items = _suggestions.ProvideFileSystemSuggestions(inputValue)
|
||||
.Prepend(new InputEntry(InputEntryType.Text, inputValue, inputValue));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
302
ViewModel.cs
302
ViewModel.cs
@ -1,10 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using Microsoft.Win32;
|
||||
using TechnologyAssembler.Core.IO;
|
||||
|
||||
namespace HashCalculator.GUI
|
||||
{
|
||||
@ -37,47 +43,305 @@ namespace HashCalculator.GUI
|
||||
#endregion
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1812")]
|
||||
internal class ViewModel : NotifyPropertyChanged
|
||||
{
|
||||
public InputBarViewModel MainInput { get; } = new InputBarViewModel();
|
||||
public InputBarViewModel BigEntryInput { get; } = new InputBarViewModel();
|
||||
public MainInputViewModel MainInput { get; } = new MainInputViewModel();
|
||||
public BigInputViewModel BigEntryInput { get; } = new BigInputViewModel();
|
||||
public InputBarViewModel AssetIdInput { get; } = new InputBarViewModel();
|
||||
public Model Model { get; }
|
||||
|
||||
private Visibility _bigInputVisibility = Visibility.Collapsed;
|
||||
public Visibility BigInputVisibility
|
||||
{
|
||||
get => _bigInputVisibility;
|
||||
set => SetField(ref _bigInputVisibility, value);
|
||||
}
|
||||
|
||||
private InputEntry? _mainInputResult; // if not null
|
||||
private string? _bigManifest;
|
||||
|
||||
public ViewModel()
|
||||
{
|
||||
Model = new Model(this);
|
||||
}
|
||||
|
||||
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
|
||||
public async Task OnMainInputDecided(InputEntry selected)
|
||||
{
|
||||
BigInputVisibility = Visibility.Collapsed;
|
||||
switch (selected.Type)
|
||||
{
|
||||
case InputEntryType.BinaryFile:
|
||||
CopyableBox.ShowDialog(async cancel =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var hash = await Task.Run(() =>
|
||||
{
|
||||
return SageHash.CalculateBinaryHash(File.ReadAllBytes(selected.Value));
|
||||
}).ConfigureAwait(false);
|
||||
return $"使用Sage Hash计算出的哈希值:{hash:X8} (十进制 {hash})\r\n"
|
||||
+ "注意这是以大小写敏感模式计算出的哈希值,与素材ID(大小写不敏感)的哈希其实并不完全一样";
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return exception.ToString();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case InputEntryType.BigFile:
|
||||
BigInputVisibility = Visibility.Visible;
|
||||
throw new NotImplementedException();
|
||||
case InputEntryType.ManifestFile:
|
||||
await ProcessManifest(selected).ConfigureAwait(true);
|
||||
break;
|
||||
case InputEntryType.XmlFile:
|
||||
throw new NotImplementedException();
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ProcessManifest(InputEntry entry)
|
||||
{
|
||||
var content = await Task.Run(() =>
|
||||
{
|
||||
using var stream = File.OpenRead(entry.Value);
|
||||
return new ManifestContent(entry.Text, stream);
|
||||
}).ConfigureAwait(true);
|
||||
await ProcessManifest(content).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
public async Task ProcessManifest(ManifestContent content)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal class InputBarViewModel : NotifyPropertyChanged
|
||||
{
|
||||
private IEnumerable<InputEntry>? _items;
|
||||
public IEnumerable<InputEntry>? Items
|
||||
public virtual IEnumerable<InputEntry>? Items
|
||||
{
|
||||
get => _items;
|
||||
set => SetField(ref _items, value);
|
||||
}
|
||||
|
||||
private InputEntry? selectedItem;
|
||||
public InputEntry? SelectedItem
|
||||
private InputEntry? _selectedItem;
|
||||
public virtual InputEntry? SelectedItem
|
||||
{
|
||||
get { return selectedItem; }
|
||||
set { SetField(ref selectedItem, value); }
|
||||
}
|
||||
|
||||
private string? selectedValue;
|
||||
public string? SelectedValue
|
||||
{
|
||||
get { return selectedValue; }
|
||||
set { SetField(ref selectedValue, value); }
|
||||
get => _selectedItem;
|
||||
set => SetField(ref _selectedItem, value);
|
||||
}
|
||||
|
||||
private string? _text;
|
||||
public string? Text
|
||||
public virtual string? Text
|
||||
{
|
||||
get { return _text; }
|
||||
set { SetField(ref _text, value); }
|
||||
get => _text;
|
||||
set => SetField(ref _text, value);
|
||||
}
|
||||
}
|
||||
|
||||
internal class MainInputViewModel : InputBarViewModel
|
||||
{
|
||||
private readonly FileSystemSuggestions _suggestions = new FileSystemSuggestions();
|
||||
|
||||
private int _selectedIndex;
|
||||
public int SelectedIndex
|
||||
{
|
||||
get => _selectedIndex;
|
||||
set
|
||||
{
|
||||
if (value == 0)
|
||||
{
|
||||
if (Items.Count() <= 1)
|
||||
{
|
||||
value = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = _selectedIndex < value ? 1 : -1;
|
||||
}
|
||||
}
|
||||
SetField(ref _selectedIndex, value);
|
||||
}
|
||||
}
|
||||
|
||||
public override string? Text
|
||||
{
|
||||
get => base.Text;
|
||||
set
|
||||
{
|
||||
base.Text = value;
|
||||
if (value != SelectedItem?.ToString())
|
||||
{
|
||||
UpdateList(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Command<MainWindow> BrowseCommand { get; }
|
||||
public Command<ViewModel> SelectCommand { get; }
|
||||
|
||||
public MainInputViewModel()
|
||||
{
|
||||
Items = Enumerable.Empty<InputEntry>();
|
||||
BrowseCommand = new Command<MainWindow>(window =>
|
||||
{
|
||||
var dialog = new OpenFileDialog
|
||||
{
|
||||
Multiselect = false,
|
||||
ValidateNames = true
|
||||
};
|
||||
if (dialog.ShowDialog(window) == true)
|
||||
{
|
||||
Text = dialog.FileName;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
SelectCommand = new Command<ViewModel>(async viewModel =>
|
||||
{
|
||||
if(SelectedItem?.IsValid != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
BrowseCommand.CanExecuteValue = false;
|
||||
SelectCommand.CanExecuteValue = false;
|
||||
await viewModel.OnMainInputDecided(SelectedItem).ConfigureAwait(true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BrowseCommand.CanExecuteValue = true;
|
||||
SelectCommand.CanExecuteValue = true;
|
||||
SelectedIndex = -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateList(string? path)
|
||||
{
|
||||
bool candidateFound = false;
|
||||
var result = _suggestions.ProvideFileSystemSuggestions(path);
|
||||
InputEntry? first = result.FirstOrDefault();
|
||||
if (first != null)
|
||||
{
|
||||
if (new FileInfo(path!).FullName == new FileInfo(first.Value).FullName)
|
||||
{
|
||||
candidateFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
result = result.Prepend(new InputEntry(InputEntryType.Text, path!, path!));
|
||||
}
|
||||
Items = result;
|
||||
|
||||
if (candidateFound)
|
||||
{
|
||||
SelectedIndex = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("Design", "CA1001:具有可释放字段的类型应该是可释放的", Justification = "<挂起>")]
|
||||
internal class BigInputViewModel : NotifyPropertyChanged
|
||||
{
|
||||
private CancellationTokenSource? _currentCancellator = null;
|
||||
|
||||
private IEnumerable<ManifestContent>? _manifests;
|
||||
public IEnumerable<ManifestContent>? Manifests
|
||||
{
|
||||
get => _manifests;
|
||||
set => SetField(ref _manifests, value);
|
||||
}
|
||||
|
||||
private ManifestContent? _selectedManifest;
|
||||
public ManifestContent? SelectedManifest
|
||||
{
|
||||
get => _selectedManifest;
|
||||
set
|
||||
{
|
||||
SetField(ref _selectedManifest, value);
|
||||
SelectCommand.CanExecuteValue = value != null;
|
||||
}
|
||||
}
|
||||
|
||||
public Command<ViewModel> SelectCommand { get; }
|
||||
|
||||
public BigInputViewModel()
|
||||
{
|
||||
SelectCommand = new Command<ViewModel>(viewModel => viewModel.ProcessManifest(SelectedManifest!));
|
||||
}
|
||||
|
||||
public async Task LoadBig(string path)
|
||||
{
|
||||
using var cancellator = _currentCancellator = new CancellationTokenSource();
|
||||
var saved = Interlocked.Exchange(ref _currentCancellator, cancellator);
|
||||
saved?.Cancel();
|
||||
|
||||
var token = cancellator.Token;
|
||||
try
|
||||
{
|
||||
using var big = await Task.Run(() => new BigFile(path), token).ConfigureAwait(true);
|
||||
var manifests = await Task.Run(() =>
|
||||
{
|
||||
var manifests = big.GetFiles(string.Empty, "*.manifest", VirtualSearchOptionType.AllDirectories);
|
||||
var modManifests = from manifest in manifests
|
||||
where FileNameStartsWith(manifest, "mod")
|
||||
select manifest;
|
||||
var globalDataManifests = from manifest in manifests
|
||||
where FileNameStartsWith(manifest, "mapmetadata")
|
||||
select manifest;
|
||||
var firstManifests = modManifests.Concat(globalDataManifests);
|
||||
var otherManifests = from manifest in manifests
|
||||
where !firstManifests.Contains(manifest)
|
||||
select manifest;
|
||||
|
||||
var list = new List<ManifestContent>();
|
||||
foreach (var path in firstManifests.Concat(otherManifests))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
using var stream = big.OpenStream(path);
|
||||
list.Add(new ManifestContent(path, stream));
|
||||
}
|
||||
return list;
|
||||
}, token).ConfigureAwait(true);
|
||||
|
||||
if (!token.IsCancellationRequested)
|
||||
{
|
||||
Manifests = manifests;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
|
||||
Interlocked.CompareExchange(ref _currentCancellator, null, cancellator);
|
||||
}
|
||||
|
||||
private static bool FileNameStartsWith(string path, string what)
|
||||
{
|
||||
return Path.GetFileName(path).StartsWith(what, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
internal class ManifestContent
|
||||
{
|
||||
public string Name { get; }
|
||||
public byte[] Data { get; }
|
||||
|
||||
public ManifestContent(string name, Stream stream)
|
||||
{
|
||||
Name = name;
|
||||
using var memoryStream = new MemoryStream();
|
||||
stream.CopyTo(memoryStream);
|
||||
Data = memoryStream.ToArray();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user