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.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(ref X field, X value, [CallerMemberName] string? propertyName = null) { if (propertyName == null) { throw new InvalidOperationException(); } if (EqualityComparer.Default.Equals(field, value)) { return; } field = value; Notify(propertyName); } #endregion } [SuppressMessage("Microsoft.Performance", "CA1812")] internal class ViewModel : NotifyPropertyChanged { private static readonly Random _random = new(); public event EventHandler? ClearTracer; public event EventHandler? 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); } public Command FilterDisplayEntries { get; } private int _totalCount; public int TotalCount { get => _totalCount; set => SetField(ref _totalCount, value); } public ObservableSortedCollection DisplayEntries { get; } = new ObservableSortedCollection(); 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); } private bool _isLoadingXml; public bool IsLoadingXml { get => _isLoadingXml; set => SetField(ref _isLoadingXml, value); } public Command CancelXml { get; } [SuppressMessage("Microsoft.Performance", "CA1822")] public string LoadCsfText => $"{(Translator.HasProvider ? "更换" : "加载")} CSF"; public Command 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(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 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 items) { DisplayEntries.SortedAddItems(items.Where(FilterDisplayEntry)); } public void FilterCollection(HashSet 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.IndexOf(chunk, StringComparison.OrdinalIgnoreCase) != -1) { return true; } if (entry.InstanceIdString.IndexOf(chunk, StringComparison.OrdinalIgnoreCase) != -1) { return true; } if (Translator.HasProvider) { if (entry.LocalizedNames.IndexOf(chunk, StringComparison.CurrentCultureIgnoreCase) != -1) { 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? _items; public virtual IEnumerable? 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 BrowseCommand { get; } public Command SelectCommand { get; } public MainInputViewModel(Controller controller) { Items = Enumerable.Empty(); BrowseCommand = new Command(async window => { var fileName = await controller.RequestOpenFile(string.Empty); if (fileName is not null) { Text = fileName; } }); SelectCommand = new Command(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? 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? _allManifests; public IEnumerable? AllManifests { get => _allManifests; set { SetField(ref _allManifests, value); Items = value; SelectedItem = null; LastProcessedManifest = null; Text = null; } } public Command SelectCommand { get; } public BigInputViewModel(Controller controller) { SelectCommand = new Command(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) { CopyableBox.ShowDialog("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 : ObservableCollection where T : IComparable { private readonly HashSet _set = new HashSet(); public void SortedAddItems(IEnumerable 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 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; } } }