From b9adceed951b50dc9c88c4ed5c35d34534b36b25 Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Mon, 14 Jul 2025 13:50:33 +0100 Subject: [PATCH] [App] Make importing DAT folder multithreaded. --- RomRepoMgr/Models/DatImporter.cs | 99 +++++ RomRepoMgr/Resources/Localization.Designer.cs | 6 + RomRepoMgr/Resources/Localization.es.resx | 3 + RomRepoMgr/Resources/Localization.resx | 3 + .../ViewModels/ImportDatFolderViewModel.cs | 377 +++++++----------- RomRepoMgr/ViewModels/MainWindowViewModel.cs | 20 +- RomRepoMgr/Views/ImportDatFolder.axaml | 236 ++++++----- RomRepoMgr/Views/ImportDatFolder.axaml.cs | 39 +- 8 files changed, 389 insertions(+), 394 deletions(-) create mode 100644 RomRepoMgr/Models/DatImporter.cs diff --git a/RomRepoMgr/Models/DatImporter.cs b/RomRepoMgr/Models/DatImporter.cs new file mode 100644 index 0000000..f560167 --- /dev/null +++ b/RomRepoMgr/Models/DatImporter.cs @@ -0,0 +1,99 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Media; +using ReactiveUI; +using RomRepoMgr.Core.EventArgs; + +namespace RomRepoMgr.Models; + +public class DatImporter : ReactiveObject +{ + bool _indeterminate; + double _maximum; + double _minimum; + double _progress; + Color _statusColor; + string _statusMessage; + public string Filename { get; internal init; } + public Task Task { get; set; } + public bool Running { get; private set; } = true; + + public bool Indeterminate + { + get => _indeterminate; + set => this.RaiseAndSetIfChanged(ref _indeterminate, value); + } + + public double Progress + { + get => _progress; + set => this.RaiseAndSetIfChanged(ref _progress, value); + } + + public double Maximum + { + get => _maximum; + set => this.RaiseAndSetIfChanged(ref _maximum, value); + } + + public double Minimum + { + get => _minimum; + set => this.RaiseAndSetIfChanged(ref _minimum, value); + } + + public string StatusMessage + { + get => _statusMessage; + set => this.RaiseAndSetIfChanged(ref _statusMessage, value); + } + + public Color StatusColor + { + get => _statusColor; + set => this.RaiseAndSetIfChanged(ref _statusColor, value); + } + + internal void OnErrorOccurred(object sender, ErrorEventArgs e) + { + StatusMessage = e.Message; + StatusColor = Colors.Red; + + if(!Indeterminate) return; + + Indeterminate = false; + Progress = 0; + } + + internal void OnSetIndeterminateProgress(object sender, EventArgs e) + { + Indeterminate = true; + } + + internal void OnSetMessage(object sender, MessageEventArgs e) + { + StatusMessage = e.Message; + } + + internal void OnSetProgress(object sender, ProgressEventArgs e) + { + Progress = e.Value; + } + + internal void OnSetProgressBounds(object sender, ProgressBoundsEventArgs e) + { + Indeterminate = false; + Maximum = e.Maximum; + Minimum = e.Minimum; + } + + internal void OnWorkFinished(object sender, MessageEventArgs e) + { + Indeterminate = false; + Maximum = 1; + Minimum = 0; + Progress = 1; + StatusMessage = e.Message; + Running = false; + } +} \ No newline at end of file diff --git a/RomRepoMgr/Resources/Localization.Designer.cs b/RomRepoMgr/Resources/Localization.Designer.cs index 915fb04..adb2134 100644 --- a/RomRepoMgr/Resources/Localization.Designer.cs +++ b/RomRepoMgr/Resources/Localization.Designer.cs @@ -764,5 +764,11 @@ namespace RomRepoMgr.Resources { return ResourceManager.GetString("NativeMenuQuitText", resourceCulture); } } + + public static string ProgressLabel { + get { + return ResourceManager.GetString("ProgressLabel", resourceCulture); + } + } } } diff --git a/RomRepoMgr/Resources/Localization.es.resx b/RomRepoMgr/Resources/Localization.es.resx index 2e00b57..3afe466 100644 --- a/RomRepoMgr/Resources/Localization.es.resx +++ b/RomRepoMgr/Resources/Localization.es.resx @@ -378,4 +378,7 @@ Tardará mucho tiempo... _Salir + + Progreso + \ No newline at end of file diff --git a/RomRepoMgr/Resources/Localization.resx b/RomRepoMgr/Resources/Localization.resx index 03c4577..89c9059 100644 --- a/RomRepoMgr/Resources/Localization.resx +++ b/RomRepoMgr/Resources/Localization.resx @@ -386,4 +386,7 @@ This will take a long time... _Quit + + Progress + \ No newline at end of file diff --git a/RomRepoMgr/ViewModels/ImportDatFolderViewModel.cs b/RomRepoMgr/ViewModels/ImportDatFolderViewModel.cs index b62fa10..b6504aa 100644 --- a/RomRepoMgr/ViewModels/ImportDatFolderViewModel.cs +++ b/RomRepoMgr/ViewModels/ImportDatFolderViewModel.cs @@ -1,89 +1,71 @@ -/****************************************************************************** -// RomRepoMgr - ROM repository manager -// ---------------------------------------------------------------------------- -// -// Author(s) : Natalia Portillo -// -// --[ License ] -------------------------------------------------------------- -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// -// ---------------------------------------------------------------------------- -// Copyright © 2020-2024 Natalia Portillo -*******************************************************************************/ - using System; +using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reactive; using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Platform.Storage; using Avalonia.Threading; using ReactiveUI; using RomRepoMgr.Core.EventArgs; -using RomRepoMgr.Core.Workers; +using RomRepoMgr.Models; using RomRepoMgr.Resources; -using RomRepoMgr.Views; -using ErrorEventArgs = RomRepoMgr.Core.EventArgs.ErrorEventArgs; namespace RomRepoMgr.ViewModels; -public sealed class ImportDatFolderViewModel : ViewModelBase +public class ImportDatFolderViewModel : ViewModelBase { - readonly ImportDatFolder _view; - bool _allFilesChecked; - bool _canClose; - bool _canStart; - string _category; - string[] _datFiles; - bool _isImporting; - bool _isReady; - int _listPosition; - bool _progress2IsIndeterminate; - double _progress2Maximum; - double _progress2Minimum; - double _progress2Value; - bool _progress2Visible; - bool _progressIsIndeterminate; - double _progressMaximum; - double _progressMinimum; - double _progressValue; - bool _progressVisible; - bool _recursiveChecked; - string _status2Message; - string _statusMessage; + readonly Stopwatch _stopwatch = new(); + bool _allFilesChecked; + bool _canClose; + bool _canStart; + string _category; + string[] _datFiles; + string _folderPath; + bool _isImporting; + bool _isReady; + int _listPosition; + bool _progressIsIndeterminate; + double _progressMaximum; + double _progressMinimum; + double _progressValue; + bool _progressVisible; + bool _recursiveChecked; + string _statusMessage; + int _workers; - // Mock public ImportDatFolderViewModel() { -#pragma warning disable PH2080 - FolderPath = "C:\\ROMs"; -#pragma warning restore PH2080 + CanClose = true; + IsReady = true; + SelectFolderCommand = ReactiveCommand.CreateFromTask(SelectFolderAsync); + CloseCommand = ReactiveCommand.Create(Close); + StartCommand = ReactiveCommand.Create(Start); } - public ImportDatFolderViewModel(ImportDatFolder view, string folderPath) + public ReactiveCommand SelectFolderCommand { get; } + public Window View { get; init; } + + public bool IsReady { - _view = view; - FolderPath = folderPath; - _allFilesChecked = false; - _recursiveChecked = true; - ImportResults = []; - CloseCommand = ReactiveCommand.Create(ExecuteCloseCommand); - StartCommand = ReactiveCommand.Create(ExecuteStartCommand); + get => _isReady; + set => this.RaiseAndSetIfChanged(ref _isReady, value); } - public string FolderPath { get; } + public string FolderPath + { + get => _folderPath; + set => this.RaiseAndSetIfChanged(ref _folderPath, value); + } + + public string Category + { + get => _category; + set => this.RaiseAndSetIfChanged(ref _category, value); + } public bool AllFilesChecked { @@ -105,24 +87,36 @@ public sealed class ImportDatFolderViewModel : ViewModelBase } } - public bool IsReady - { - get => _isReady; - set => this.RaiseAndSetIfChanged(ref _isReady, value); - } - public bool ProgressVisible { get => _progressVisible; set => this.RaiseAndSetIfChanged(ref _progressVisible, value); } + public bool ProgressIsIndeterminate + { + get => _progressIsIndeterminate; + set => this.RaiseAndSetIfChanged(ref _progressIsIndeterminate, value); + } + public string StatusMessage { get => _statusMessage; set => this.RaiseAndSetIfChanged(ref _statusMessage, value); } + public bool CanClose + { + get => _canClose; + set => this.RaiseAndSetIfChanged(ref _canClose, value); + } + + public bool CanStart + { + get => _canStart; + set => this.RaiseAndSetIfChanged(ref _canStart, value); + } + public double ProgressMinimum { get => _progressMinimum; @@ -141,78 +135,113 @@ public sealed class ImportDatFolderViewModel : ViewModelBase set => this.RaiseAndSetIfChanged(ref _progressValue, value); } - public bool ProgressIsIndeterminate - { - get => _progressIsIndeterminate; - set => this.RaiseAndSetIfChanged(ref _progressIsIndeterminate, value); - } - - public bool Progress2Visible - { - get => _progress2Visible; - set => this.RaiseAndSetIfChanged(ref _progress2Visible, value); - } - - public string Status2Message - { - get => _status2Message; - set => this.RaiseAndSetIfChanged(ref _status2Message, value); - } - - public double Progress2Minimum - { - get => _progress2Minimum; - set => this.RaiseAndSetIfChanged(ref _progress2Minimum, value); - } - - public double Progress2Maximum - { - get => _progress2Maximum; - set => this.RaiseAndSetIfChanged(ref _progress2Maximum, value); - } - - public double Progress2Value - { - get => _progress2Value; - set => this.RaiseAndSetIfChanged(ref _progress2Value, value); - } - - public bool Progress2IsIndeterminate - { - get => _progress2IsIndeterminate; - set => this.RaiseAndSetIfChanged(ref _progress2IsIndeterminate, value); - } - public bool IsImporting { get => _isImporting; set => this.RaiseAndSetIfChanged(ref _isImporting, value); } - public string Category + public ReactiveCommand CloseCommand { get; } + public ReactiveCommand StartCommand { get; } + public ObservableCollection Importers { get; } = []; + + void Start() { - get => _category; - set => this.RaiseAndSetIfChanged(ref _category, value); + _listPosition = 0; + ProgressMinimum = 0; + ProgressMaximum = _datFiles.Length; + ProgressValue = 0; + ProgressIsIndeterminate = false; + ProgressVisible = true; + CanClose = false; + CanStart = false; + IsReady = false; + IsImporting = true; + _workers = 0; + _stopwatch.Restart(); + + Import(); } - public ObservableCollection ImportResults { get; } - - public bool CanClose + void Import() { - get => _canClose; - set => this.RaiseAndSetIfChanged(ref _canClose, value); + Dispatcher.UIThread.Post(() => + { + if(_listPosition >= _datFiles.Length) + { + if(_workers != 0) return; + + ProgressVisible = false; + StatusMessage = Localization.Finished; + CanClose = true; + CanStart = false; + IsReady = true; + _stopwatch.Stop(); + + return; + } + + StatusMessage = string.Format(Localization.ImportingItem, Path.GetFileName(_datFiles[_listPosition])); + ProgressValue = _listPosition; + + var model = new DatImporter + { + Filename = Path.GetFileName(_datFiles[_listPosition]), + Minimum = 0, + Maximum = _datFiles.Length, + Progress = 0, + Indeterminate = false + }; + + var worker = new Core.Workers.DatImporter(_datFiles[_listPosition], Category); + worker.ErrorOccurred += model.OnErrorOccurred; + worker.SetIndeterminateProgress += model.OnSetIndeterminateProgress; + worker.SetMessage += model.OnSetMessage; + worker.SetProgress += model.OnSetProgress; + worker.SetProgressBounds += model.OnSetProgressBounds; + worker.WorkFinished += model.OnWorkFinished; + worker.RomSetAdded += RomSetAdded; + + worker.WorkFinished += (_, _) => + { + _workers--; + + if(_workers < Environment.ProcessorCount) Import(); + }; + + Importers.Add(model); + + model.Task = Task.Run(worker.Import); + + _workers++; + _listPosition++; + + if(_workers < Environment.ProcessorCount) Import(); + }); } - public bool CanStart + public event EventHandler RomSetAdded; + + void Close() { - get => _canStart; - set => this.RaiseAndSetIfChanged(ref _canStart, value); + View.Close(); } - public ReactiveCommand CloseCommand { get; } - public ReactiveCommand StartCommand { get; } + async Task SelectFolderAsync() + { + IReadOnlyList result = + await View.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + Title = Localization.ImportDatFolderDialogTitle + }); - internal void OnOpened() => RefreshFiles(); + if(result.Count < 1) return; + + FolderPath = result[0].TryGetLocalPath() ?? string.Empty; + RecursiveChecked = true; + AllFilesChecked = false; + RefreshFiles(); + } void RefreshFiles() { @@ -222,7 +251,6 @@ public sealed class ImportDatFolderViewModel : ViewModelBase { IsReady = false; ProgressVisible = true; - Progress2Visible = false; ProgressIsIndeterminate = true; StatusMessage = Localization.SearchingForFiles; }); @@ -264,99 +292,4 @@ public sealed class ImportDatFolderViewModel : ViewModelBase }); }); } - - void ExecuteCloseCommand() => _view.Close(); - - void ExecuteStartCommand() - { - _listPosition = 0; - ProgressMinimum = 0; - ProgressMaximum = _datFiles.Length; - ProgressValue = 0; - ProgressIsIndeterminate = false; - ProgressVisible = true; - Progress2Visible = true; - CanClose = false; - CanStart = false; - IsReady = false; - IsImporting = true; - - Import(); - } - - void Import() - { - if(_listPosition >= _datFiles.Length) - { - Progress2Visible = false; - ProgressVisible = false; - StatusMessage = Localization.Finished; - CanClose = true; - CanStart = false; - IsReady = true; - - return; - } - - StatusMessage = string.Format(Localization.ImportingItem, Path.GetFileName(_datFiles[_listPosition])); - ProgressValue = _listPosition; - - var worker = new DatImporter(_datFiles[_listPosition], Category); - worker.ErrorOccurred += OnWorkerOnErrorOccurred; - worker.SetIndeterminateProgress += OnWorkerOnSetIndeterminateProgress; - worker.SetMessage += OnWorkerOnSetMessage; - worker.SetProgress += OnWorkerOnSetProgress; - worker.SetProgressBounds += OnWorkerOnSetProgressBounds; - worker.WorkFinished += OnWorkerOnWorkFinished; - worker.RomSetAdded += RomSetAdded; - _ = Task.Run(worker.Import); - } - - void OnWorkerOnWorkFinished(object sender, MessageEventArgs args) => Dispatcher.UIThread.Post(() => - { - ImportResults.Add(new ImportDatFolderItem - { - Filename = Path.GetFileName(_datFiles[_listPosition]), - Status = args.Message - }); - - _listPosition++; - Import(); - }); - - void OnWorkerOnSetProgressBounds(object sender, ProgressBoundsEventArgs args) => Dispatcher.UIThread.Post(() => - { - Progress2IsIndeterminate = false; - Progress2Maximum = args.Maximum; - Progress2Minimum = args.Minimum; - }); - - void OnWorkerOnSetProgress(object sender, ProgressEventArgs args) => - Dispatcher.UIThread.Post(() => Progress2Value = args.Value); - - void OnWorkerOnSetMessage(object sender, MessageEventArgs args) => - Dispatcher.UIThread.Post(() => Status2Message = args.Message); - - void OnWorkerOnSetIndeterminateProgress(object sender, EventArgs args) => - Dispatcher.UIThread.Post(() => Progress2IsIndeterminate = true); - - void OnWorkerOnErrorOccurred(object sender, ErrorEventArgs args) => Dispatcher.UIThread.Post(() => - { - ImportResults.Add(new ImportDatFolderItem - { - Filename = Path.GetFileName(_datFiles[_listPosition]), - Status = args.Message - }); - - _listPosition++; - Import(); - }); - - public event EventHandler RomSetAdded; -} - -public sealed class ImportDatFolderItem -{ - public string Filename { get; set; } - public string Status { get; set; } } \ No newline at end of file diff --git a/RomRepoMgr/ViewModels/MainWindowViewModel.cs b/RomRepoMgr/ViewModels/MainWindowViewModel.cs index 236e80d..c27b81e 100644 --- a/RomRepoMgr/ViewModels/MainWindowViewModel.cs +++ b/RomRepoMgr/ViewModels/MainWindowViewModel.cs @@ -156,19 +156,17 @@ public class MainWindowViewModel : ViewModelBase async Task ExecuteImportDatFolderCommandAsync() { - IReadOnlyList result = - await _view.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions - { - Title = Localization.ImportDatFolderDialogTitle - }); + var dialog = new ImportDatFolder(); - if(result.Count < 1) return; + var viewModel = new ImportDatFolderViewModel + { + View = dialog + }; - var dialog = new ImportDatFolder(); - var importDatFolderViewModel = new ImportDatFolderViewModel(dialog, result[0].Path.LocalPath); - importDatFolderViewModel.RomSetAdded += ImportDatViewModelOnRomSetAdded; - dialog.DataContext = importDatFolderViewModel; - _ = dialog.ShowDialog(_view); + viewModel.RomSetAdded += ImportDatViewModelOnRomSetAdded; + + dialog.DataContext = viewModel; + _ = dialog.ShowDialog(_view); } async Task ExecuteImportRomFolderCommandAsync() diff --git a/RomRepoMgr/Views/ImportDatFolder.axaml b/RomRepoMgr/Views/ImportDatFolder.axaml index e9d47ff..c40da27 100644 --- a/RomRepoMgr/Views/ImportDatFolder.axaml +++ b/RomRepoMgr/Views/ImportDatFolder.axaml @@ -1,139 +1,125 @@ - + Icon="/Assets/avalonia-logo.ico" + CanResize="False"> - - - - - - - - - - - - - - - - - - - - - + + - - + VerticalAlignment="Center" /> + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/RomRepoMgr/Views/ImportDatFolder.axaml.cs b/RomRepoMgr/Views/ImportDatFolder.axaml.cs index c5fc3b4..1a67122 100644 --- a/RomRepoMgr/Views/ImportDatFolder.axaml.cs +++ b/RomRepoMgr/Views/ImportDatFolder.axaml.cs @@ -1,44 +1,11 @@ -/****************************************************************************** -// RomRepoMgr - ROM repository manager -// ---------------------------------------------------------------------------- -// -// Author(s) : Natalia Portillo -// -// --[ License ] -------------------------------------------------------------- -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// -// ---------------------------------------------------------------------------- -// Copyright © 2020-2024 Natalia Portillo -*******************************************************************************/ - -using System; using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using RomRepoMgr.ViewModels; namespace RomRepoMgr.Views; -public sealed partial class ImportDatFolder : Window +public partial class ImportDatFolder : Window { - public ImportDatFolder() => InitializeComponent(); - - void InitializeComponent() => AvaloniaXamlLoader.Load(this); - - protected override void OnOpened(EventArgs e) + public ImportDatFolder() { - base.OnOpened(e); - (DataContext as ImportDatFolderViewModel)?.OnOpened(); + InitializeComponent(); } } \ No newline at end of file