ui, and controller, ecc, ecc
This commit is contained in:
parent
1d706b1adc
commit
97067a68a5
162
App.xaml
162
App.xaml
@ -1,9 +1,167 @@
|
||||
<Application x:Class="HashCalculator.GUI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:HashCalculator.GUI"
|
||||
xmlns:l="clr-namespace:HashCalculator.GUI"
|
||||
xmlns:c="clr-namespace:HashCalculator.GUI.Converters"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
|
||||
<c:ValueConverterAggregate x:Key="ValidInputEntryToVisibilityConverter">
|
||||
<c:ValidInputEntryTypeToBooleanConverter />
|
||||
<BooleanToVisibilityConverter />
|
||||
</c:ValueConverterAggregate>
|
||||
<c:ValueConverterAggregate x:Key="InvalidInputEntryToVisibilityConverter">
|
||||
<c:ValidInputEntryTypeToBooleanConverter />
|
||||
<c:BooleanInvertConverter />
|
||||
<BooleanToVisibilityConverter />
|
||||
</c:ValueConverterAggregate>
|
||||
<c:ValueConverterAggregate x:Key="NullToVisibilityConverter">
|
||||
<c:NullToBooleanConverter />
|
||||
<c:BooleanInvertConverter />
|
||||
<BooleanToVisibilityConverter />
|
||||
</c:ValueConverterAggregate>
|
||||
<c:ValueConverterAggregate x:Key="NotNullToVisibilityConverter">
|
||||
<c:NullToBooleanConverter />
|
||||
<BooleanToVisibilityConverter />
|
||||
</c:ValueConverterAggregate>
|
||||
<c:ValueConverterAggregate x:Key="NonZeroToVisibilityConverter">
|
||||
<c:IsZeroToBooleanConverter />
|
||||
<BooleanToVisibilityConverter />
|
||||
</c:ValueConverterAggregate>
|
||||
<c:MultiValueEqualityConverter x:Key="MultiValueEqualityConverter" />
|
||||
<Style x:Key="CommonStyle" TargetType="Control">
|
||||
<Setter
|
||||
Property="Foreground"
|
||||
Value="LightGray"
|
||||
/>
|
||||
<Setter
|
||||
Property="Background"
|
||||
Value="#202020"
|
||||
/>
|
||||
</Style>
|
||||
<Style
|
||||
BasedOn="{StaticResource CommonStyle}"
|
||||
TargetType="l:MainWindow"
|
||||
/>
|
||||
<Style
|
||||
TargetType="l:ShellLink"
|
||||
>
|
||||
<Setter
|
||||
Property="Foreground"
|
||||
Value="#30B0FF"
|
||||
/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Foreground" Value="Red" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style
|
||||
BasedOn="{StaticResource CommonStyle}"
|
||||
TargetType="{x:Type TextBox}"
|
||||
>
|
||||
<Setter Property="Background" Value="#00000000"/>
|
||||
</Style>
|
||||
<Style
|
||||
BasedOn="{StaticResource CommonStyle}"
|
||||
TargetType="{x:Type ComboBox}"
|
||||
>
|
||||
<Setter Property="Background" Value="#00000000"/>
|
||||
</Style>
|
||||
<Style
|
||||
BasedOn="{StaticResource CommonStyle}"
|
||||
TargetType="{x:Type Button}"
|
||||
x:Key="ButtonStyle"
|
||||
>
|
||||
<Setter Property="Background" Value="#20808080"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type Button}">
|
||||
<Border
|
||||
x:Name="ButtonBorderTemplate"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Background="{TemplateBinding Background}"
|
||||
SnapsToDevicePixels="True"
|
||||
>
|
||||
<ContentPresenter
|
||||
x:Name="ButtonContentPresenterTemplate"
|
||||
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="ButtonBorderTemplate" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" TargetName="ButtonBorderTemplate" Value="#80BEE6FD"/>
|
||||
<Setter Property="BorderBrush" TargetName="ButtonBorderTemplate" Value="#FF3C7FB1"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" TargetName="ButtonBorderTemplate" Value="#80C4E5F6"/>
|
||||
<Setter Property="BorderBrush" TargetName="ButtonBorderTemplate" Value="#FF2C628B"/>
|
||||
</Trigger>
|
||||
<Trigger Property="ToggleButton.IsChecked" Value="True">
|
||||
<Setter Property="Background" TargetName="ButtonBorderTemplate" Value="#FFBCDDEE"/>
|
||||
<Setter Property="BorderBrush" TargetName="ButtonBorderTemplate" Value="#FF245A83"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Background" TargetName="ButtonBorderTemplate" Value="#00000000"/>
|
||||
<Setter Property="BorderBrush" TargetName="ButtonBorderTemplate" Value="#FFADB2B5"/>
|
||||
<Setter Property="Foreground" Value="#808080"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style
|
||||
BasedOn="{StaticResource ButtonStyle}"
|
||||
TargetType="Button"
|
||||
/>
|
||||
<Style
|
||||
BasedOn="{StaticResource CommonStyle}"
|
||||
TargetType="CheckBox"
|
||||
/>
|
||||
<Style
|
||||
BasedOn="{StaticResource CommonStyle}"
|
||||
TargetType="{x:Type DataGrid}"
|
||||
/>
|
||||
<Style
|
||||
BasedOn="{StaticResource CommonStyle}"
|
||||
TargetType="{x:Type DataGridColumnHeader}"
|
||||
>
|
||||
<Setter Property="Padding" Value="10,5" />
|
||||
<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 DataGridRowHeader}"
|
||||
/>
|
||||
<Style
|
||||
BasedOn="{StaticResource CommonStyle}"
|
||||
TargetType="{x:Type DataGridRow}"
|
||||
/>
|
||||
<Style TargetType="{x:Type Button}" x:Key="{ComponentResourceKey ResourceId=DataGridSelectAllButtonStyle, TypeInTargetAssembly={x:Type DataGrid}}">
|
||||
<Setter Property="Background" Value="Black" />
|
||||
</Style>
|
||||
<Style
|
||||
BasedOn="{StaticResource CommonStyle}"
|
||||
TargetType="{x:Type ListBox}"
|
||||
>
|
||||
<Setter Property="Background" Value="#181818"/>
|
||||
</Style>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
@ -5,6 +5,7 @@ using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using TechnologyAssembler;
|
||||
|
||||
namespace HashCalculator.GUI
|
||||
{
|
||||
@ -13,5 +14,10 @@ namespace HashCalculator.GUI
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
public App()
|
||||
{
|
||||
var core = new TechnologyAssemblerCoreModule();
|
||||
core.Initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using TechnologyAssembler.Core.Assets;
|
||||
|
||||
namespace HashCalculator.GUI
|
||||
{
|
||||
@ -10,10 +12,10 @@ namespace HashCalculator.GUI
|
||||
public string Type { get; }
|
||||
public string Name { get; }
|
||||
public IEnumerable<string> DisplayLabels { get; }
|
||||
public uint Hash => SageHash.CalculateLowercaseHash(Name);
|
||||
public uint InstanceId => SageHash.CalculateLowercaseHash(Name);
|
||||
|
||||
public string IdString => $"{Type}:{Name}";
|
||||
public string HashString => $"{Hash:0X} ({Hash})";
|
||||
public string NameString => $"{Type}:{Name}";
|
||||
public string InstanceIdString => $"{InstanceId:X8} ({InstanceId})";
|
||||
|
||||
public AssetEntry(XElement element)
|
||||
{
|
||||
@ -38,6 +40,21 @@ namespace HashCalculator.GUI
|
||||
DisplayLabels = labels.Concat(transformLabels).ToArray();
|
||||
}
|
||||
|
||||
public AssetEntry(Asset asset)
|
||||
{
|
||||
if(asset is null)
|
||||
{
|
||||
throw new ArgumentNullException($"{nameof(asset)} is null");
|
||||
}
|
||||
Type = asset.TypeName;
|
||||
Name = asset.InstanceName;
|
||||
DisplayLabels = Enumerable.Empty<string>();
|
||||
if(InstanceId != asset.InstanceId || SageHash.CalculateBinaryHash(Type) != asset.TypeId)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(AssetEntry entry)
|
||||
{
|
||||
return this == entry;
|
||||
@ -73,7 +90,7 @@ namespace HashCalculator.GUI
|
||||
return true;
|
||||
}
|
||||
|
||||
return a.Type == b.Type && a.Hash == b.Hash;
|
||||
return a.Type == b.Type && a.InstanceId == b.InstanceId;
|
||||
}
|
||||
|
||||
public static bool operator !=(AssetEntry a, AssetEntry b)
|
||||
@ -84,7 +101,34 @@ namespace HashCalculator.GUI
|
||||
// override object.GetHashCode
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Type, Hash);
|
||||
return HashCode.Combine(Type, InstanceId);
|
||||
}
|
||||
}
|
||||
|
||||
internal class DisplayAssetEntry : IComparable<DisplayAssetEntry>
|
||||
{
|
||||
public string Name { get; }
|
||||
public string InstanceId { get; }
|
||||
|
||||
public DisplayAssetEntry(AssetEntry entry)
|
||||
{
|
||||
Name = entry.NameString;
|
||||
InstanceId = entry.InstanceIdString;
|
||||
}
|
||||
|
||||
public int CompareTo(DisplayAssetEntry other)
|
||||
{
|
||||
return string.CompareOrdinal(Name, other.Name);
|
||||
}
|
||||
}
|
||||
|
||||
internal class LocalizedDisplayAssetEntry : DisplayAssetEntry
|
||||
{
|
||||
public string LocalizedNames { get; }
|
||||
|
||||
public LocalizedDisplayAssetEntry(AssetEntry entry) : base(entry)
|
||||
{
|
||||
LocalizedNames = entry.DisplayLabels.Aggregate((x, y) => $"{x} {y}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
227
Controller.cs
Normal file
227
Controller.cs
Normal file
@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TechnologyAssembler.Core.IO;
|
||||
using TechnologyAssembler.Core.Assets;
|
||||
|
||||
namespace HashCalculator.GUI
|
||||
{
|
||||
[SuppressMessage("Design", "CA1001:具有可释放字段的类型应该是可释放的", Justification = "<挂起>")]
|
||||
internal class Controller
|
||||
{
|
||||
public const string SelectedFileSystemRootPath = "/selected";
|
||||
public const string ManifestExtension = ".manifest";
|
||||
|
||||
private ViewModel ViewModel { get; }
|
||||
private BigFileSystemProvider? _currentBig = null;
|
||||
|
||||
public Controller(ViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
}
|
||||
|
||||
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
|
||||
public async Task OnMainInputDecided(InputEntry selected)
|
||||
{
|
||||
_currentBig?.Dispose();
|
||||
_currentBig = null;
|
||||
ViewModel.Entries.Clear();
|
||||
ViewModel.TraceText = string.Empty;
|
||||
ViewModel.BigEntryInput.AllManifests = null;
|
||||
|
||||
try
|
||||
{
|
||||
switch (selected.Type)
|
||||
{
|
||||
case InputEntryType.BinaryFile:
|
||||
ViewModel.StatusText = "请留意一下弹出的窗口(";
|
||||
CopyableBox.ShowDialog(token => CalculateBinaryHash(selected.Value, token));
|
||||
ViewModel.StatusText = ViewModel.SuggestionString(string.Empty);
|
||||
return;
|
||||
case InputEntryType.BigFile:
|
||||
await LoadBig(selected.Value).ConfigureAwait(true);
|
||||
return;
|
||||
case InputEntryType.ManifestFile:
|
||||
await LoadManifestFromFile(selected.Value).ConfigureAwait(true);
|
||||
return;
|
||||
case InputEntryType.XmlFile:
|
||||
throw new NotImplementedException();
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
ViewModel.StatusText = "失败…";
|
||||
CopyableBox.ShowDialog(_ =>
|
||||
{
|
||||
return Task.FromResult($"在尝试加载 {selected.Type} `{selected.Value}` 时发生错误:\r\n{error}");
|
||||
});
|
||||
ViewModel.StatusText = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
|
||||
private static Task<string> CalculateBinaryHash(string filePath, CancellationToken cancel)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var file = File.OpenRead(filePath);
|
||||
var array = new byte[file.Length];
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
var totalRead = 0;
|
||||
const int ReadSize = 32 * 1024 * 1024;
|
||||
while (totalRead < array.Length)
|
||||
{
|
||||
totalRead += file.Read(array, totalRead, Math.Min(array.Length - totalRead, ReadSize));
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
var hashes = new Func<byte[], uint>[]
|
||||
{
|
||||
SageHash.CalculateBinaryHash,
|
||||
SageHash.CalculateLauncherBinaryHash
|
||||
}.AsParallel().Select(fn => fn(array)).ToArray();
|
||||
|
||||
return $"使用 SAGE FastHash 计算出的哈希值:{hashes[0]:X8} (十进制 {hashes[0]})\r\n"
|
||||
+ "注意这是以大小写敏感模式计算出的哈希值,与素材ID的哈希值(转换成小写后计算的哈希)一般是不一样的\r\n\r\n"
|
||||
+ "使用 SAGE Launcher FastHash(比如说 RA3.exe 用来校验更新补丁文件的哈希)计算出的哈希值:"
|
||||
+ $"{hashes[1]:X8} (十进制 {hashes[1]})";
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return exception.ToString();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task LoadBig(string path)
|
||||
{
|
||||
ViewModel.StatusText = "正在尝试加载 big 文件…";
|
||||
var bigViewModel = ViewModel.BigEntryInput;
|
||||
bigViewModel.Text = string.Empty;
|
||||
bigViewModel.AllManifests = await Task.Run(() =>
|
||||
{
|
||||
using var big = new BigFile(path);
|
||||
_currentBig = new BigFileSystemProvider(SelectedFileSystemRootPath, big, null);
|
||||
return FindManifests();
|
||||
}).ConfigureAwait(true);
|
||||
|
||||
var first = bigViewModel.AllManifests.FirstOrDefault();
|
||||
if (first != null)
|
||||
{
|
||||
var name = VirtualFileSystem.GetFileNameWithoutExtension(first.Value);
|
||||
if (name.StartsWith("mod", StringComparison.OrdinalIgnoreCase)
|
||||
|| name.StartsWith("mapmetadata", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
bigViewModel.SelectedItem = first;
|
||||
await bigViewModel.SelectCommand.ExecuteTask(ViewModel).ConfigureAwait(true);
|
||||
ViewModel.StatusText = "big 已加载完毕,并自动选了一个看起来比较合适的 manifest 文件(";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewModel.StatusText = "big 文件已加载完毕,请选择 big 文件里的 manifest 文件";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadManifestFromFile(string path)
|
||||
{
|
||||
using var provider = new FileSystemProvider(SelectedFileSystemRootPath, null);
|
||||
await ProcessManifest(path).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
public async Task ProcessManifest(string path)
|
||||
{
|
||||
ViewModel.Entries.Clear();
|
||||
ViewModel.TraceText = string.Empty;
|
||||
ViewModel.StatusText = "正在加载 manifest 文件…";
|
||||
var entries = await Task.Run(() =>
|
||||
{
|
||||
if (path.EndsWith(ManifestExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
path = path.Remove(path.Length - ManifestExtension.Length);
|
||||
}
|
||||
using var manifest = Manifest.Load(path);
|
||||
var assets = from asset in manifest.Assets.Values
|
||||
select new DisplayAssetEntry(new AssetEntry(asset));
|
||||
return assets.ToArray();
|
||||
}).ConfigureAwait(true);
|
||||
await AddEntries(entries).ConfigureAwait(true);
|
||||
ViewModel.StatusText = $"总共加载了{ViewModel.Entries.Count}个素材";
|
||||
}
|
||||
|
||||
public async Task AddEntries(IEnumerable<DisplayAssetEntry> entries)
|
||||
{
|
||||
var target = ViewModel.Entries;
|
||||
|
||||
int BinarySearch(DisplayAssetEntry entry, int? hint = null)
|
||||
{
|
||||
var begin = hint.GetValueOrDefault(0);
|
||||
var end = target.Count;
|
||||
while (begin < end)
|
||||
{
|
||||
var middle = begin + (end - begin) / 2;
|
||||
var result = entry.CompareTo(target[middle]);
|
||||
if (result == 0)
|
||||
{
|
||||
return middle;
|
||||
}
|
||||
else if (result < 0)
|
||||
{
|
||||
end = middle;
|
||||
}
|
||||
else if (result > 0)
|
||||
{
|
||||
begin = middle + 1;
|
||||
}
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
var ordered = await Task.Run(() =>
|
||||
{
|
||||
var array = entries.ToArray();
|
||||
Array.Sort(array);
|
||||
return array;
|
||||
}).ConfigureAwait(true);
|
||||
ViewModel.StatusText = $"正在处理刚刚读取出来的 {ordered.Length} 个素材";
|
||||
var hint = 0;
|
||||
foreach (var entry in ordered)
|
||||
{
|
||||
var index = hint = BinarySearch(entry, hint);
|
||||
target.Insert(index, entry);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<InputEntry> FindManifests()
|
||||
{
|
||||
var manifests = VirtualFileSystem.ListFiles(SelectedFileSystemRootPath, "*.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;
|
||||
|
||||
return from manifest in firstManifests.Concat(otherManifests)
|
||||
select new InputEntry(InputEntryType.ManifestFile, manifest, manifest);
|
||||
}
|
||||
|
||||
private static bool FileNameStartsWith(string path, string what)
|
||||
{
|
||||
return VirtualFileSystem.GetFileName(path).StartsWith(what, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace HashCalculator.GUI.Converters
|
||||
{
|
||||
[SuppressMessage("Microsoft.Performance", "CA1812")]
|
||||
[ValueConversion(typeof(bool), typeof(bool))]
|
||||
internal class BooleanInvertConverter : IValueConverter
|
||||
{
|
||||
|
20
Converters/IsZeroToBooleanConverter.cs
Normal file
20
Converters/IsZeroToBooleanConverter.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace HashCalculator.GUI.Converters
|
||||
{
|
||||
[ValueConversion(typeof(int), typeof(bool))]
|
||||
public class IsZeroToBooleanConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return (int)value == 0 ? false : true;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
22
Converters/MultiValueEqualityConverter.cs
Normal file
22
Converters/MultiValueEqualityConverter.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace HashCalculator.GUI.Converters
|
||||
{
|
||||
[SuppressMessage("Microsoft.Performance", "CA1812")]
|
||||
internal class MultiValueEqualityConverter : IMultiValueConverter
|
||||
{
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return values.All(x => Equals(x, values.First()));
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace HashCalculator.GUI.Converters
|
||||
{
|
||||
[SuppressMessage("Microsoft.Performance", "CA1812")]
|
||||
[ValueConversion(typeof(InputEntry), typeof(bool))]
|
||||
internal class ValidInputEntryTypeToBooleanConverter : IValueConverter
|
||||
{
|
||||
|
@ -1,11 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace HashCalculator.GUI.Converters
|
||||
{
|
||||
[SuppressMessage("Microsoft.Performance", "CA1812")]
|
||||
internal class ValueConverterAggregate : List<IValueConverter>, IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
|
@ -128,32 +128,22 @@ namespace HashCalculator.GUI
|
||||
|
||||
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
|
||||
{
|
||||
using var timer = new DisposableDispatcherTimer(timer =>
|
||||
{
|
||||
Text = $"{InitialMessage}\r\n目前耗时{timer.TimeSinceCreation},稍微再等一下吧233";
|
||||
}, dispatcher, TimeSpan.FromMilliseconds(200), TimeSpan.FromSeconds(1));
|
||||
|
||||
return await action(token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return "操作已被取消";
|
||||
}
|
||||
finally
|
||||
{
|
||||
timer.Stop();
|
||||
}
|
||||
}
|
||||
Text = await dispatcher.Invoke(Action).ConfigureAwait(true);
|
||||
}
|
||||
|
38
DisposableDispatcherTimer.cs
Normal file
38
DisposableDispatcherTimer.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace HashCalculator.GUI
|
||||
{
|
||||
internal class DisposableDispatcherTimer : IDisposable
|
||||
{
|
||||
public DispatcherTimer Timer { get; }
|
||||
public DateTimeOffset CreationTime { get; } = DateTimeOffset.UtcNow;
|
||||
public TimeSpan TimeSinceCreation => DateTimeOffset.UtcNow - CreationTime;
|
||||
|
||||
public DisposableDispatcherTimer(Action<DisposableDispatcherTimer> action, Dispatcher dispatcher, TimeSpan interval) :
|
||||
this(action, dispatcher, interval, TimeSpan.Zero)
|
||||
{
|
||||
}
|
||||
|
||||
public DisposableDispatcherTimer(Action<DisposableDispatcherTimer> action, Dispatcher dispatcher, TimeSpan interval, TimeSpan wait)
|
||||
{
|
||||
Timer = new DispatcherTimer(interval, DispatcherPriority.Normal, (s, e) =>
|
||||
{
|
||||
if (Timer.IsEnabled && (TimeSinceCreation > wait))
|
||||
{
|
||||
action(this);
|
||||
}
|
||||
}, dispatcher);
|
||||
Timer.Start();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Timer.Stop();
|
||||
}
|
||||
}
|
||||
}
|
@ -44,15 +44,16 @@ namespace HashCalculator.GUI
|
||||
|
||||
var currentFiles = new List<InputEntry>();
|
||||
string? fileName;
|
||||
string? currentFullPath;
|
||||
string? currentFullPath = null;
|
||||
try
|
||||
{
|
||||
currentFullPath = Path.GetFullPath(path);
|
||||
fileName = Path.GetFileName(path);
|
||||
if (File.Exists(currentFullPath))
|
||||
var currentFile = new FileInfo(path);
|
||||
fileName = currentFile.Name;
|
||||
if (currentFile.Exists)
|
||||
{
|
||||
var type = CheckExtension(currentFullPath);
|
||||
if(type is InputEntryType entryType)
|
||||
currentFullPath = currentFile.FullName;
|
||||
var type = CheckExtension(currentFile);
|
||||
if (type is InputEntryType entryType)
|
||||
{
|
||||
currentFiles.Add(new InputEntry(entryType, path, currentFullPath));
|
||||
}
|
||||
@ -77,7 +78,7 @@ namespace HashCalculator.GUI
|
||||
select file;
|
||||
|
||||
var supportedFiles = from file in otherFiles
|
||||
let type = CheckExtension(file.Extension)
|
||||
let type = CheckExtension(file)
|
||||
where type.HasValue
|
||||
select new InputEntry(type.Value, _search.GetInputStyleName(file), file.FullName);
|
||||
|
||||
@ -91,9 +92,9 @@ namespace HashCalculator.GUI
|
||||
return currentFiles.Concat(alternatives);
|
||||
}
|
||||
|
||||
private static InputEntryType? CheckExtension(string path)
|
||||
private static InputEntryType? CheckExtension(FileInfo info)
|
||||
{
|
||||
if (Mapping.TryGetValue(path, out var type))
|
||||
if (Mapping.TryGetValue(info.Extension, out var type))
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
@ -20,11 +20,11 @@
|
||||
<PackageReference Include="Mvp.Xml" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TechnologyAssembler.Core\TechnologyAssembler.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Windows.Presentation" />
|
||||
<Reference Include="TechnologyAssembler.Core">
|
||||
<HintPath>TechnologyAssembler.Core.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
@ -8,7 +8,7 @@
|
||||
xmlns:c="clr-namespace:HashCalculator.GUI.Converters"
|
||||
mc:Ignorable="d"
|
||||
x:ClassModifier="internal"
|
||||
x:Name="_this"
|
||||
x:Name="Self"
|
||||
d:DesignHeight="100" d:DesignWidth="800"
|
||||
>
|
||||
<UserControl.Resources>
|
||||
@ -69,12 +69,17 @@
|
||||
<Grid>
|
||||
<!--TextChanged="OnTextChanged"-->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="24"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
x:Name="TextBox"
|
||||
Text="{Binding Path=Text,
|
||||
RelativeSource={RelativeSource AncestorType=local:InputBar},
|
||||
UpdateSourceTrigger=PropertyChanged}"
|
||||
PreviewKeyDown="OnPreviewKeyDown"
|
||||
Grid.Column="0"
|
||||
Padding="2, 0"
|
||||
VerticalContentAlignment="Center"
|
||||
LostFocus="OnLostFocus"
|
||||
@ -84,9 +89,20 @@
|
||||
RelativeSource={RelativeSource AncestorType=local:InputBar}}"
|
||||
IsHitTestVisible="False"
|
||||
Visibility="{Binding ElementName=TextBox, Path=Text, Converter={StaticResource NullToVisibilityConverter}}"
|
||||
Grid.Column="0"
|
||||
Padding="5, 0"
|
||||
VerticalAlignment="Center"
|
||||
/>
|
||||
<Button
|
||||
Click="OnToggleDropDownButtonClick"
|
||||
Grid.Column="1"
|
||||
Background="Transparent"
|
||||
>
|
||||
<Path
|
||||
Fill="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
|
||||
Data="M 0 2 L 1 0 L 6 4 L 11 0 L 12 2 L 6 7 Z"
|
||||
/>
|
||||
</Button>
|
||||
</Grid>
|
||||
<Popup
|
||||
x:Name="DropDown"
|
||||
@ -101,7 +117,6 @@
|
||||
ItemsSource="{Binding Path=Collection,
|
||||
RelativeSource={RelativeSource AncestorType=local:InputBar}}"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="OnSelectionChanged"
|
||||
SelectedItem="{Binding Path=SelectedItem,
|
||||
RelativeSource={RelativeSource AncestorType=local:InputBar}}"
|
||||
SelectedIndex="{Binding Path=SelectedIndex,
|
||||
|
@ -7,13 +7,9 @@ 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.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace HashCalculator.GUI
|
||||
{
|
||||
@ -74,7 +70,12 @@ namespace HashCalculator.GUI
|
||||
DependencyProperty.Register(nameof(SelectedItem), typeof(InputEntry), typeof(InputBar), new FrameworkPropertyMetadata
|
||||
{
|
||||
BindsTwoWayByDefault = true,
|
||||
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
|
||||
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
|
||||
PropertyChangedCallback = (d, e) =>
|
||||
{
|
||||
var self = (InputBar)d;
|
||||
self.OnSelectionChanged();
|
||||
}
|
||||
});
|
||||
public InputEntry? SelectedItem
|
||||
{
|
||||
@ -127,7 +128,7 @@ namespace HashCalculator.GUI
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
private void OnSelectionChanged()
|
||||
{
|
||||
if (SelectedItem != null)
|
||||
{
|
||||
@ -151,5 +152,14 @@ namespace HashCalculator.GUI
|
||||
return Math.Min(Math.Max(rawIndex, -1), Collection.Count() - 1);
|
||||
}
|
||||
|
||||
private void OnToggleDropDownButtonClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
TextBox.Focus();
|
||||
TextBox.Select(Text?.Length ?? 0, 0);
|
||||
if (!DropDown.IsOpen)
|
||||
{
|
||||
DropDown.IsOpen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
261
MainWindow.xaml
261
MainWindow.xaml
@ -8,106 +8,18 @@
|
||||
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"
|
||||
Title="SAGE FastHash 哈希计算器"
|
||||
Height="600"
|
||||
Width="600"
|
||||
Style="{StaticResource CommonStyle}"
|
||||
>
|
||||
<Window.Resources>
|
||||
<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>
|
||||
<Window.DataContext>
|
||||
<l:ViewModel/>
|
||||
<l:ViewModel />
|
||||
</Window.DataContext>
|
||||
<Grid Margin="10,20,10,10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="130" />
|
||||
<RowDefinition Height="150" />
|
||||
</Grid.RowDefinitions>
|
||||
<DockPanel Grid.Row="0">
|
||||
<Grid
|
||||
@ -150,85 +62,152 @@
|
||||
DockPanel.Dock="Top"
|
||||
Height="25"
|
||||
Margin="0,10,0,0"
|
||||
Visibility="Visible"
|
||||
Visibility="{Binding BigEntryInput.AllManifests, Converter={StaticResource NotNullToVisibilityConverter}}"
|
||||
>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="92"/>
|
||||
<ColumnDefinition Width="92" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<l:InputBar
|
||||
HintText="输入 big 文件里包含的 manifest 文件的路径"
|
||||
Collection="{Binding Items}"
|
||||
SelectedItem="{Binding SelectedItem}"
|
||||
Text="{Binding BigEntryInput.Text}"
|
||||
Collection="{Binding BigEntryInput.Items}"
|
||||
SelectedItem="{Binding BigEntryInput.SelectedItem}"
|
||||
SelectedIndex="{Binding BigEntryInput.SelectedIndex}"
|
||||
Grid.Column="0"
|
||||
Margin="0,0,10,0"
|
||||
/>
|
||||
<Button
|
||||
Content="确认"
|
||||
Command="{Binding BigEntryInput.SelectCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
Grid.Column="1"
|
||||
Margin="0"
|
||||
Foreground="ForestGreen"
|
||||
BorderBrush="ForestGreen" Template="{DynamicResource ButtonBaseControlTemplate1}"
|
||||
/>
|
||||
>
|
||||
<Button.Style>
|
||||
<Style
|
||||
BasedOn="{StaticResource ButtonStyle}"
|
||||
TargetType="Button"
|
||||
>
|
||||
<Style.Triggers>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Value="False">
|
||||
<Condition.Binding>
|
||||
<MultiBinding Converter="{StaticResource MultiValueEqualityConverter}">
|
||||
<Binding Path="BigEntryInput.LastProcessedManifest"></Binding>
|
||||
<Binding Path="BigEntryInput.SelectedItem"></Binding>
|
||||
</MultiBinding>
|
||||
</Condition.Binding>
|
||||
</Condition>
|
||||
<Condition
|
||||
Binding="{Binding RelativeSource={RelativeSource Mode=Self},
|
||||
Path=IsEnabled}"
|
||||
Value="True"
|
||||
/>
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter Property="Foreground" Value="#20FF30" />
|
||||
<Setter Property="BorderBrush" Value="#20FF30" />
|
||||
</MultiDataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
</Grid>
|
||||
<DockPanel
|
||||
<Grid
|
||||
DockPanel.Dock="Top"
|
||||
Height="25"
|
||||
Height="50"
|
||||
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"
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Text="{Binding StatusText}"
|
||||
Grid.Row="0"
|
||||
Margin="10,0"
|
||||
VerticalAlignment="Center"
|
||||
/>
|
||||
<TextBlock
|
||||
Text="正在加载……"
|
||||
Text="{Binding Entries.Count, StringFormat=目前加载了{0}个素材}"
|
||||
Grid.Row="1"
|
||||
Margin="10,5,0,5"
|
||||
Width="160"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Grid.Column="0"
|
||||
Visibility="{Binding Entries.Count, Converter={StaticResource NonZeroToVisibilityConverter}}"
|
||||
/>
|
||||
</DockPanel>
|
||||
<l:InputBar
|
||||
HintText="过滤Asset ID(可选)"
|
||||
Collection="{Binding Items}"
|
||||
SelectedItem="{Binding SelectedItem}"
|
||||
|
||||
<CheckBox
|
||||
Content="只显示 GameObject"
|
||||
IsChecked="True"
|
||||
Grid.Row="1"
|
||||
Margin="0,5,242,5"
|
||||
Width="155"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
/>
|
||||
<Button
|
||||
Content="加载 csf/mod.str"
|
||||
Grid.Row="1"
|
||||
Width="140"
|
||||
Margin="0,0,97,0"
|
||||
HorizontalAlignment="Right"
|
||||
/>
|
||||
<Button
|
||||
Content="取消加载"
|
||||
Grid.Row="1"
|
||||
Width="92"
|
||||
HorizontalAlignment="Right"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
DockPanel.Dock="Top"
|
||||
Height="25"
|
||||
/>
|
||||
>
|
||||
<TextBox></TextBox>
|
||||
<TextBlock
|
||||
Text="过滤Asset ID(可选)"
|
||||
Padding="5,0"
|
||||
VerticalAlignment="Center"
|
||||
/>
|
||||
</Grid>
|
||||
<DataGrid
|
||||
x:Name="DataGrid"
|
||||
AutoGenerateColumns="False"
|
||||
ItemsSource="{Binding Entries}"
|
||||
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>
|
||||
/>
|
||||
</DockPanel>
|
||||
<TextBlock
|
||||
x:Name="textBlock"
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="0,10,0,0"
|
||||
TextWrapping="Wrap"
|
||||
ScrollViewer.CanContentScroll="True"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
>
|
||||
本工具基于 <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>
|
||||
从而能在 .NET Standard 2.0 上使用 C# 8.0 的末尾索引操作符<LineBreak />
|
||||
<LineBreak />
|
||||
假如你对本工具有任何疑问或者建议的话,可以来到<l:ShellLink NavigateUri="https://tieba.baidu.com/ra3">红警3吧</l:ShellLink>发帖寻找岚依(
|
||||
</TextBlock>
|
||||
<TextBlock 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>
|
||||
从而能在 .NET Standard 2.0 上使用 C# 8.0 的末尾索引操作符<LineBreak />
|
||||
<LineBreak />
|
||||
假如你对本工具有任何疑问或者建议的话,可以来到<l:ShellLink NavigateUri="https://tieba.baidu.com/ra3">红警3吧</l:ShellLink>发帖寻找岚依(
|
||||
</TextBlock>
|
||||
<TextBlock />
|
||||
<StackPanel
|
||||
Height="25"
|
||||
HorizontalAlignment="Right"
|
||||
>
|
||||
<Button
|
||||
Content="清除输出"
|
||||
Width="92"
|
||||
Grid.Row="1"
|
||||
/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
@ -21,65 +21,37 @@ namespace HashCalculator.GUI
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
internal ViewModel ViewModel => (ViewModel)DataContext;
|
||||
private bool _autoscroll = true;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnMainInputTextChanged(object sender, TextChangedEventArgs e)
|
||||
private void OnButtomScrollViewerScrollChanged(object sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
CopyableBox.ShowDialog(async token =>
|
||||
{
|
||||
var init = DateTimeOffset.UtcNow;
|
||||
var i = 0;
|
||||
while (i < 100)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
++i;
|
||||
var scrollViewer = (ScrollViewer)e.Source;
|
||||
// User scroll event : set or unset auto-scroll mode
|
||||
if (e.ExtentHeightChange == 0)
|
||||
{ // Content unchanged : user scroll event
|
||||
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
|
||||
{ // Scroll bar is in bottom
|
||||
// Set auto-scroll mode
|
||||
_autoscroll = true;
|
||||
}
|
||||
MessageBox.Show("Completed!");
|
||||
return $"Completed after exactly {DateTimeOffset.UtcNow - init}";
|
||||
});
|
||||
/*var mainInput = _viewModel.MainInput;
|
||||
if (sender is InputBar)
|
||||
{
|
||||
var inputValue = mainInput.Text;
|
||||
if(inputValue != null)
|
||||
{
|
||||
mainInput.Items = _suggestions.ProvideFileSystemSuggestions(inputValue)
|
||||
.Prepend(new InputEntry(InputEntryType.Text, inputValue, inputValue));
|
||||
else
|
||||
{ // Scroll bar isn't in bottom
|
||||
// Unset auto-scroll mode
|
||||
_autoscroll = false;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBigInputTextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
/*var mainInput = _viewModel.MainInput;
|
||||
if (sender is InputBar)
|
||||
{
|
||||
var inputValue = mainInput.Text;
|
||||
if (inputValue != null)
|
||||
{
|
||||
mainInput.Items = _suggestions.ProvideFileSystemSuggestions(inputValue)
|
||||
.Prepend(new InputEntry(InputEntryType.Text, inputValue, inputValue));
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
private void OnAssetInputTextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
/*var mainInput = _viewModel.MainInput;
|
||||
if (sender is InputBar)
|
||||
{
|
||||
var inputValue = mainInput.Text;
|
||||
if (inputValue != null)
|
||||
{
|
||||
mainInput.Items = _suggestions.ProvideFileSystemSuggestions(inputValue)
|
||||
.Prepend(new InputEntry(InputEntryType.Text, inputValue, inputValue));
|
||||
}
|
||||
}*/
|
||||
// Content scroll event : auto-scroll eventually
|
||||
if (_autoscroll && e.ExtentHeightChange != 0)
|
||||
{ // Content changed and auto-scroll mode set
|
||||
// Autoscroll
|
||||
scrollViewer.ScrollToVerticalOffset(scrollViewer.ExtentHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +0,0 @@
|
||||
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("");
|
||||
}
|
||||
}
|
||||
}
|
@ -18,6 +18,15 @@ namespace HashCalculator.GUI
|
||||
return FastHash.GetHashCode(content);
|
||||
}
|
||||
|
||||
public static uint CalculateLauncherBinaryHash(byte[] content)
|
||||
{
|
||||
if (content == null)
|
||||
{
|
||||
throw new ArgumentNullException($"{nameof(content)} is null");
|
||||
}
|
||||
return FastHash.GetHashCodeLauncher(0, content);
|
||||
}
|
||||
|
||||
public static uint CalculateBinaryHash(string content)
|
||||
{
|
||||
if (content == null)
|
||||
|
270
ViewModel.cs
270
ViewModel.cs
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
@ -46,74 +47,52 @@ namespace HashCalculator.GUI
|
||||
[SuppressMessage("Microsoft.Performance", "CA1812")]
|
||||
internal class ViewModel : NotifyPropertyChanged
|
||||
{
|
||||
public MainInputViewModel MainInput { get; } = new MainInputViewModel();
|
||||
public BigInputViewModel BigEntryInput { get; } = new BigInputViewModel();
|
||||
public InputBarViewModel AssetIdInput { get; } = new InputBarViewModel();
|
||||
private static readonly Random _random = new Random();
|
||||
|
||||
private Visibility _bigInputVisibility = Visibility.Collapsed;
|
||||
public Visibility BigInputVisibility
|
||||
public MainInputViewModel MainInput { get; }
|
||||
public BigInputViewModel BigEntryInput { get; }
|
||||
|
||||
private ObservableCollection<DisplayAssetEntry> _entries = new ObservableCollection<DisplayAssetEntry>();
|
||||
public ObservableCollection<DisplayAssetEntry> Entries
|
||||
{
|
||||
get => _bigInputVisibility;
|
||||
set => SetField(ref _bigInputVisibility, value);
|
||||
get => _entries;
|
||||
set => SetField(ref _entries, value);
|
||||
}
|
||||
|
||||
private InputEntry? _mainInputResult; // if not null
|
||||
private string? _bigManifest;
|
||||
private string _statusText = SuggestionString("不知道该显示些什么呢……");
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set => SetField(ref _statusText, value);
|
||||
}
|
||||
|
||||
private string _traceText = string.Empty;
|
||||
public string TraceText
|
||||
{
|
||||
get => _traceText;
|
||||
set => SetField(ref _traceText, value);
|
||||
}
|
||||
|
||||
public ViewModel()
|
||||
{
|
||||
var controller = new Controller(this);
|
||||
MainInput = new MainInputViewModel(controller);
|
||||
BigEntryInput = new BigInputViewModel(controller);
|
||||
}
|
||||
|
||||
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
|
||||
public async Task OnMainInputDecided(InputEntry selected)
|
||||
public static string SuggestionString(string original)
|
||||
{
|
||||
BigInputVisibility = Visibility.Collapsed;
|
||||
switch (selected.Type)
|
||||
var generated = $"{_random.NextDouble()}";
|
||||
System.Diagnostics.Debug.WriteLine(generated);
|
||||
if (generated.Contains("38") || generated.Contains("16"))
|
||||
{
|
||||
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();
|
||||
return "你们都是喂鱼的马甲!(";
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ProcessManifest(InputEntry entry)
|
||||
{
|
||||
var content = await Task.Run(() =>
|
||||
if (generated.IndexOf('2') == 2)
|
||||
{
|
||||
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();
|
||||
return "本来以为两小时就能写完这个小工具,没想到写了两个星期,开始怀疑自己的智商orz";
|
||||
}
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,6 +112,13 @@ namespace HashCalculator.GUI
|
||||
set => SetField(ref _selectedItem, value);
|
||||
}
|
||||
|
||||
private int _selectedIndex;
|
||||
public virtual int SelectedIndex
|
||||
{
|
||||
get => _selectedIndex;
|
||||
set => SetField(ref _selectedIndex, value);
|
||||
}
|
||||
|
||||
private string? _text;
|
||||
public virtual string? Text
|
||||
{
|
||||
@ -145,10 +131,9 @@ namespace HashCalculator.GUI
|
||||
{
|
||||
private readonly FileSystemSuggestions _suggestions = new FileSystemSuggestions();
|
||||
|
||||
private int _selectedIndex;
|
||||
public int SelectedIndex
|
||||
public override int SelectedIndex
|
||||
{
|
||||
get => _selectedIndex;
|
||||
get => base.SelectedIndex;
|
||||
set
|
||||
{
|
||||
if (value == 0)
|
||||
@ -159,10 +144,10 @@ namespace HashCalculator.GUI
|
||||
}
|
||||
else
|
||||
{
|
||||
value = _selectedIndex < value ? 1 : -1;
|
||||
value = base.SelectedIndex < value ? 1 : -1;
|
||||
}
|
||||
}
|
||||
SetField(ref _selectedIndex, value);
|
||||
base.SelectedIndex = value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,7 +167,7 @@ namespace HashCalculator.GUI
|
||||
public Command<MainWindow> BrowseCommand { get; }
|
||||
public Command<ViewModel> SelectCommand { get; }
|
||||
|
||||
public MainInputViewModel()
|
||||
public MainInputViewModel(Controller controller)
|
||||
{
|
||||
Items = Enumerable.Empty<InputEntry>();
|
||||
BrowseCommand = new Command<MainWindow>(window =>
|
||||
@ -200,7 +185,7 @@ namespace HashCalculator.GUI
|
||||
});
|
||||
SelectCommand = new Command<ViewModel>(async viewModel =>
|
||||
{
|
||||
if(SelectedItem?.IsValid != true)
|
||||
if (SelectedItem?.IsValid != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -209,7 +194,7 @@ namespace HashCalculator.GUI
|
||||
{
|
||||
BrowseCommand.CanExecuteValue = false;
|
||||
SelectCommand.CanExecuteValue = false;
|
||||
await viewModel.OnMainInputDecided(SelectedItem).ConfigureAwait(true);
|
||||
await controller.OnMainInputDecided(SelectedItem).ConfigureAwait(true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -246,102 +231,105 @@ namespace HashCalculator.GUI
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("Design", "CA1001:具有可释放字段的类型应该是可释放的", Justification = "<挂起>")]
|
||||
internal class BigInputViewModel : NotifyPropertyChanged
|
||||
internal class BigInputViewModel : InputBarViewModel
|
||||
{
|
||||
private CancellationTokenSource? _currentCancellator = null;
|
||||
|
||||
private IEnumerable<ManifestContent>? _manifests;
|
||||
public IEnumerable<ManifestContent>? Manifests
|
||||
public override IEnumerable<InputEntry>? Items
|
||||
{
|
||||
get => _manifests;
|
||||
set => SetField(ref _manifests, value);
|
||||
get => base.Items;
|
||||
set => base.Items = value;
|
||||
}
|
||||
|
||||
private ManifestContent? _selectedManifest;
|
||||
public ManifestContent? SelectedManifest
|
||||
public override InputEntry? SelectedItem
|
||||
{
|
||||
get => _selectedManifest;
|
||||
get => base.SelectedItem;
|
||||
set
|
||||
{
|
||||
SetField(ref _selectedManifest, value);
|
||||
base.SelectedItem = value;
|
||||
SelectCommand.CanExecuteValue = value != null;
|
||||
}
|
||||
}
|
||||
|
||||
public override string? Text
|
||||
{
|
||||
get => base.Text;
|
||||
set
|
||||
{
|
||||
base.Text = value;
|
||||
if (value != SelectedItem?.ToString())
|
||||
{
|
||||
UpdateList(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private InputEntry? _lastProcessedManifest;
|
||||
public InputEntry? LastProcessedManifest
|
||||
{
|
||||
get => _lastProcessedManifest;
|
||||
set => SetField(ref _lastProcessedManifest, value);
|
||||
}
|
||||
|
||||
private IEnumerable<InputEntry>? _allManifests;
|
||||
public IEnumerable<InputEntry>? AllManifests
|
||||
{
|
||||
get => _allManifests;
|
||||
set
|
||||
{
|
||||
SetField(ref _allManifests, value);
|
||||
Items = value;
|
||||
LastProcessedManifest = null;
|
||||
Text = null;
|
||||
}
|
||||
}
|
||||
|
||||
public Command<ViewModel> SelectCommand { get; }
|
||||
|
||||
public BigInputViewModel()
|
||||
[SuppressMessage("Design", "CA1031:不捕获常规异常类型", Justification = "<挂起>")]
|
||||
public BigInputViewModel(Controller controller)
|
||||
{
|
||||
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
|
||||
SelectCommand = new Command<ViewModel>(async viewModel =>
|
||||
{
|
||||
using var big = await Task.Run(() => new BigFile(path), token).ConfigureAwait(true);
|
||||
var manifests = await Task.Run(() =>
|
||||
var mainInput = viewModel.MainInput;
|
||||
try
|
||||
{
|
||||
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;
|
||||
SelectCommand.CanExecuteValue = false;
|
||||
mainInput.BrowseCommand.CanExecuteValue = false;
|
||||
mainInput.SelectCommand.CanExecuteValue = false;
|
||||
LastProcessedManifest = SelectedItem;
|
||||
await controller.ProcessManifest(SelectedItem!.Value).ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
CopyableBox.ShowDialog(_ =>
|
||||
{
|
||||
return Task.FromResult($"在加载 manifest 时发生错误:{exception}");
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
SelectCommand.CanExecuteValue = true;
|
||||
mainInput.BrowseCommand.CanExecuteValue = true;
|
||||
mainInput.SelectCommand.CanExecuteValue = true;
|
||||
}
|
||||
})
|
||||
{
|
||||
CanExecuteValue = false
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateList(string? input)
|
||||
{
|
||||
input ??= string.Empty;
|
||||
input = input.Replace(VirtualFileSystem.AltDirectorySeparatorChar, VirtualFileSystem.DirectorySeparatorChar);
|
||||
|
||||
var filtered = from manifest in AllManifests
|
||||
where manifest.Value.IndexOf(input, StringComparison.OrdinalIgnoreCase) != -1
|
||||
select manifest;
|
||||
Items = filtered;
|
||||
if (Items.FirstOrDefault()?.Value.Equals(input, StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
SelectedIndex = 0;
|
||||
}
|
||||
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