591 lines
18 KiB
C#
591 lines
18 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Collections.ObjectModel;
|
||
using System.ComponentModel;
|
||
using System.Diagnostics.CodeAnalysis;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Runtime.CompilerServices;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using TechnologyAssembler.Core.IO;
|
||
|
||
namespace HashCalculator.GUI
|
||
{
|
||
internal abstract class NotifyPropertyChanged : INotifyPropertyChanged
|
||
{
|
||
#region INotifyPropertyChanged
|
||
public event PropertyChangedEventHandler? PropertyChanged;
|
||
|
||
protected void Notify(string name)
|
||
{
|
||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||
}
|
||
|
||
protected void SetField<X>(ref X field, X value, [CallerMemberName] string? propertyName = null)
|
||
{
|
||
if (propertyName == null)
|
||
{
|
||
throw new InvalidOperationException();
|
||
}
|
||
|
||
if (EqualityComparer<X>.Default.Equals(field, value))
|
||
{
|
||
return;
|
||
}
|
||
|
||
field = value;
|
||
|
||
Notify(propertyName);
|
||
}
|
||
|
||
protected void SetField<X>(ref X field, X value, Action onChange, [CallerMemberName] string? propertyName = null)
|
||
{
|
||
if (propertyName == null)
|
||
{
|
||
throw new InvalidOperationException();
|
||
}
|
||
|
||
if (EqualityComparer<X>.Default.Equals(field, value))
|
||
{
|
||
return;
|
||
}
|
||
|
||
field = value;
|
||
onChange();
|
||
Notify(propertyName);
|
||
}
|
||
#endregion
|
||
}
|
||
|
||
[SuppressMessage("Microsoft.Performance", "CA1812")]
|
||
internal class ViewModel : NotifyPropertyChanged
|
||
{
|
||
private static readonly Random _random = new();
|
||
public event EventHandler? ClearTracer;
|
||
public event EventHandler<ShowCopyableBoxEventArgs>? ShowCopyableBox;
|
||
public event EventHandler<OpenFileDialogEventArgs>? OpenFileDialog;
|
||
public MainInputViewModel MainInput { get; }
|
||
public BigInputViewModel BigEntryInput { get; }
|
||
|
||
private string? _filter;
|
||
public string? Filter
|
||
{
|
||
get => _filter;
|
||
set => SetField(ref _filter, value, () => FilterDisplayEntries.Execute(null));
|
||
}
|
||
|
||
private bool _gameObjectOnly;
|
||
public bool GameObjectOnly
|
||
{
|
||
get => _gameObjectOnly;
|
||
set => SetField(ref _gameObjectOnly, value, () => ConsiderTypeId &= !value);
|
||
}
|
||
|
||
private bool _considerTypeId;
|
||
public bool ConsiderTypeId
|
||
{
|
||
get => _considerTypeId;
|
||
set => SetField(ref _considerTypeId, value, () => GameObjectOnly &= !value);
|
||
}
|
||
|
||
public Command FilterDisplayEntries { get; }
|
||
|
||
private int _totalCount;
|
||
public int TotalCount
|
||
{
|
||
get => _totalCount;
|
||
set => SetField(ref _totalCount, value);
|
||
}
|
||
public ObservableSortedCollection<AssetEntry> DisplayEntries { get; } = new ObservableSortedCollection<AssetEntry>();
|
||
|
||
private string? _statusText;
|
||
public string StatusText
|
||
{
|
||
get => SuggestionString(_statusText);
|
||
set => SetField(ref _statusText, value);
|
||
}
|
||
|
||
private bool _isXml;
|
||
public bool IsXml
|
||
{
|
||
get => _isXml;
|
||
set => SetField(ref _isXml, value, () => IsXsd &= value);
|
||
}
|
||
private bool _isLoadingXml;
|
||
public bool IsLoadingXml
|
||
{
|
||
get => _isLoadingXml;
|
||
set => SetField(ref _isLoadingXml, value);
|
||
}
|
||
private bool _isXsd;
|
||
public bool IsXsd
|
||
{
|
||
get => _isXsd;
|
||
set => SetField(ref _isXsd, value, () =>
|
||
{
|
||
IsXml |= value;
|
||
ConsiderTypeId |= value;
|
||
});
|
||
}
|
||
public Command CancelXml { get; }
|
||
[SuppressMessage("Microsoft.Performance", "CA1822")]
|
||
public string LoadCsfText => $"{(Translator.HasProvider ? "更换" : "加载")} CSF";
|
||
public Command<MainWindow> LoadCsf { get; }
|
||
public Command ClearTraceText { get; }
|
||
|
||
public ViewModel()
|
||
{
|
||
var controller = new Controller(this);
|
||
MainInput = new MainInputViewModel(controller);
|
||
BigEntryInput = new BigInputViewModel(controller);
|
||
CancelXml = new Command(async () =>
|
||
{
|
||
try
|
||
{
|
||
CancelXml!.CanExecuteValue = false;
|
||
await controller.CancelLoadingXml().ConfigureAwait(true);
|
||
}
|
||
finally
|
||
{
|
||
CancelXml!.CanExecuteValue = true;
|
||
}
|
||
});
|
||
LoadCsf = new Command<MainWindow>(async window =>
|
||
{
|
||
try
|
||
{
|
||
LoadCsf!.CanExecuteValue = false;
|
||
var fileName = await controller.RequestOpenFile(string.Empty, ("*.csf;*.big", "CSF / BIG 文件"));
|
||
if (fileName is not null)
|
||
{
|
||
await controller.LoadCsf(fileName).ConfigureAwait(true);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
LoadCsf!.CanExecuteValue = true;
|
||
}
|
||
});
|
||
FilterDisplayEntries = new Command(() =>
|
||
{
|
||
controller.UpdateEntries();
|
||
return Task.CompletedTask;
|
||
});
|
||
ClearTraceText = new Command(() =>
|
||
{
|
||
ClearTracer?.Invoke(this, EventArgs.Empty);
|
||
return Task.CompletedTask;
|
||
});
|
||
}
|
||
|
||
public Task RequestCopyableBox(string title, Func<CancellationToken, Task<string>> getContent)
|
||
{
|
||
var data = new ShowCopyableBoxEventArgs(title, getContent);
|
||
ShowCopyableBox?.Invoke(this, data);
|
||
return data.Completion;
|
||
}
|
||
|
||
public Task<string?> RequestOpenFile(string title, IEnumerable<(string Extension, string? Description)> filters)
|
||
{
|
||
var data = new OpenFileDialogEventArgs(title, filters);
|
||
OpenFileDialog?.Invoke(this, data);
|
||
return data.Result;
|
||
}
|
||
|
||
public void NotifyCsfChange()
|
||
{
|
||
Notify(nameof(LoadCsfText));
|
||
FilterDisplayEntries.Execute(null);
|
||
}
|
||
|
||
public void AddNewItems(IEnumerable<AssetEntry> items)
|
||
{
|
||
DisplayEntries.SortedAddItems(items.Where(FilterDisplayEntry));
|
||
}
|
||
|
||
public void FilterCollection(HashSet<AssetEntry> orderedSource)
|
||
{
|
||
DisplayEntries.SortedRemoveItems(x => !orderedSource.Contains(x) || !FilterDisplayEntry(x));
|
||
DisplayEntries.SortedAddItems(orderedSource.Where(FilterDisplayEntry));
|
||
}
|
||
|
||
private bool FilterDisplayEntry(AssetEntry entry)
|
||
{
|
||
if (GameObjectOnly)
|
||
{
|
||
if (entry.Type != "GameObject")
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(Filter))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
foreach (var chunk in Filter!.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
|
||
{
|
||
if (entry.NameString.Contains(chunk, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return true;
|
||
}
|
||
if (entry.InstanceIdString.Contains(chunk, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return true;
|
||
}
|
||
if (Translator.HasProvider)
|
||
{
|
||
if (entry.LocalizedNames.IndexOf(chunk, StringComparison.CurrentCultureIgnoreCase) != -1)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
if (!GameObjectOnly && ConsiderTypeId)
|
||
{
|
||
if (entry.TypeIdString.Contains(chunk, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private static string SuggestionString(string? text)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(text))
|
||
{
|
||
return text;
|
||
}
|
||
var generated = $"{_random.NextDouble()}";
|
||
System.Diagnostics.Debug.WriteLine(generated);
|
||
if (generated.Contains("38") || generated.Contains("16"))
|
||
{
|
||
return "你们都是喂鱼的马甲!(";
|
||
}
|
||
if (generated.IndexOf('2') == 2)
|
||
{
|
||
return "本来以为两小时就能写完这个小工具,没想到写了两个星期,开始怀疑自己的智商orz";
|
||
}
|
||
if (generated.IndexOf('7') < 5)
|
||
{
|
||
return "小提示:在下方的素材列表里,选择一行或多行之后,直接按 Ctrl+C 就可以复制内容~";
|
||
}
|
||
if (generated.IndexOf('2') == 3)
|
||
{
|
||
return "温馨提示:请多留意一下自己的重工,不要让它卖自己";
|
||
}
|
||
return "不知道该显示些什么呢……";
|
||
}
|
||
}
|
||
|
||
internal class InputBarViewModel : NotifyPropertyChanged
|
||
{
|
||
private IEnumerable<InputEntry>? _items;
|
||
public virtual IEnumerable<InputEntry>? Items
|
||
{
|
||
get => _items;
|
||
set => SetField(ref _items, value);
|
||
}
|
||
|
||
private InputEntry? _selectedItem;
|
||
public virtual InputEntry? SelectedItem
|
||
{
|
||
get => _selectedItem;
|
||
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
|
||
{
|
||
get => _text;
|
||
set => SetField(ref _text, value);
|
||
}
|
||
}
|
||
|
||
internal class MainInputViewModel : InputBarViewModel
|
||
{
|
||
private readonly FileSystemSuggestions _suggestions = new FileSystemSuggestions();
|
||
|
||
public override int SelectedIndex
|
||
{
|
||
get => base.SelectedIndex;
|
||
set
|
||
{
|
||
if (value == 0)
|
||
{
|
||
if (Items?.Count() <= 1)
|
||
{
|
||
value = -1;
|
||
}
|
||
else
|
||
{
|
||
value = base.SelectedIndex < value ? 1 : -1;
|
||
}
|
||
}
|
||
base.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(Controller controller)
|
||
{
|
||
Items = Enumerable.Empty<InputEntry>();
|
||
BrowseCommand = new Command<MainWindow>(async window =>
|
||
{
|
||
var fileName = await controller.RequestOpenFile(string.Empty);
|
||
if (fileName is not null)
|
||
{
|
||
Text = fileName;
|
||
}
|
||
});
|
||
SelectCommand = new Command<ViewModel>(async viewModel =>
|
||
{
|
||
if (SelectedItem?.IsValid != true)
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
BrowseCommand.CanExecuteValue = false;
|
||
SelectCommand!.CanExecuteValue = false;
|
||
await controller.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;
|
||
}
|
||
}
|
||
}
|
||
|
||
internal class BigInputViewModel : InputBarViewModel
|
||
{
|
||
public override IEnumerable<InputEntry>? Items
|
||
{
|
||
get => base.Items;
|
||
set => base.Items = value;
|
||
}
|
||
|
||
public override InputEntry? SelectedItem
|
||
{
|
||
get => base.SelectedItem;
|
||
set
|
||
{
|
||
base.SelectedItem = value;
|
||
SelectCommand.CanExecuteValue = value != null;
|
||
}
|
||
}
|
||
|
||
public override string? Text
|
||
{
|
||
get => base.Text;
|
||
set
|
||
{
|
||
base.Text = value;
|
||
if (value != SelectedItem?.ToString())
|
||
{
|
||
if (AllManifests != null)
|
||
{
|
||
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;
|
||
SelectedItem = null;
|
||
LastProcessedManifest = null;
|
||
Text = null;
|
||
}
|
||
}
|
||
|
||
public Command<ViewModel> SelectCommand { get; }
|
||
|
||
public BigInputViewModel(Controller controller)
|
||
{
|
||
SelectCommand = new Command<ViewModel>(async viewModel =>
|
||
{
|
||
var mainInput = viewModel.MainInput;
|
||
try
|
||
{
|
||
SelectCommand!.CanExecuteValue = false;
|
||
mainInput.BrowseCommand.CanExecuteValue = false;
|
||
mainInput.SelectCommand.CanExecuteValue = false;
|
||
LastProcessedManifest = SelectedItem;
|
||
await controller.ProcessManifest(SelectedItem!.Value).ConfigureAwait(true);
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
await viewModel.RequestCopyableBox("SAGE FastHash 计算器的错误", _ =>
|
||
{
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
internal class ObservableSortedCollection<T> : ObservableCollection<T> where T : IComparable<T>
|
||
{
|
||
private readonly HashSet<T> _set = new HashSet<T>();
|
||
|
||
public void SortedAddItems(IEnumerable<T> items)
|
||
{
|
||
try
|
||
{
|
||
items = from item in items
|
||
where !_set.Contains(item)
|
||
select item;
|
||
foreach (var item in items)
|
||
{
|
||
_set.Add(item);
|
||
base.Insert(BinarySearch(item), item);
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
_set.Clear();
|
||
base.Clear();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
public void SortedRemoveItems(Func<T, bool> ifRemove)
|
||
{
|
||
try
|
||
{
|
||
foreach (var i in Enumerable.Range(0, base.Count).Reverse())
|
||
{
|
||
var item = base[i];
|
||
if (ifRemove(item))
|
||
{
|
||
_set.Remove(item);
|
||
base.RemoveAt(i);
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
_set.Clear();
|
||
base.Clear();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
private int BinarySearch(T entry)
|
||
{
|
||
var begin = 0;
|
||
var end = base.Count;
|
||
while (begin < end)
|
||
{
|
||
var middle = begin + (end - begin) / 2;
|
||
var result = entry.CompareTo(base[middle]);
|
||
if (result == 0)
|
||
{
|
||
return middle;
|
||
}
|
||
else if (result < 0)
|
||
{
|
||
end = middle;
|
||
}
|
||
else if (result > 0)
|
||
{
|
||
begin = middle + 1;
|
||
}
|
||
}
|
||
return end;
|
||
}
|
||
}
|
||
}
|