using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Reactive; using System.Threading; 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.Database; using RomRepoMgr.Database.Models; using RomRepoMgr.Models; using RomRepoMgr.Resources; namespace RomRepoMgr.ViewModels; public class ImportRomFolderViewModel : ViewModelBase { readonly Context _ctx = Context.Create(Settings.Settings.Current.DatabasePath); readonly ConcurrentBag _newDisks = []; readonly ConcurrentBag _newFiles = []; readonly ConcurrentBag _newMedias = []; readonly Stopwatch _stopwatch = new(); bool _canClose; bool _canStart; string _folderPath; bool _isImporting; bool _isReady; bool _knownOnlyChecked; int _listPosition; bool _progress2IsIndeterminate; double _progress2Maximum; double _progress2Minimum; double _progress2Value; bool _progress2Visible; bool _progressIsIndeterminate; double _progressMaximum; double _progressMinimum; double _progressValue; bool _progressVisible; bool _recurseArchivesChecked; bool _removeFilesChecked; bool _removeFilesEnabled; FileImporter _rootImporter; string _statusMessage; string _statusMessage2; bool _statusMessage2Visible; public ImportRomFolderViewModel() { SelectFolderCommand = ReactiveCommand.CreateFromTask(SelectFolderAsync); CloseCommand = ReactiveCommand.Create(Close); StartCommand = ReactiveCommand.Create(Start); CanClose = true; RemoveFilesChecked = false; KnownOnlyChecked = true; RecurseArchivesChecked = Settings.Settings.UnArUsable; RemoveFilesEnabled = false; } public ReactiveCommand SelectFolderCommand { get; } public ReactiveCommand CloseCommand { get; } public ReactiveCommand StartCommand { get; } public Window View { get; init; } public bool RecurseArchivesEnabled => Settings.Settings.UnArUsable; public bool RemoveFilesChecked { get => _removeFilesChecked; set => this.RaiseAndSetIfChanged(ref _removeFilesChecked, value); } public bool KnownOnlyChecked { get => _knownOnlyChecked; set => this.RaiseAndSetIfChanged(ref _knownOnlyChecked, value); } public bool RemoveFilesEnabled { get => _removeFilesEnabled; set => this.RaiseAndSetIfChanged(ref _removeFilesEnabled, value); } public bool RecurseArchivesChecked { get => _recurseArchivesChecked; set { if(value) RemoveFilesChecked = false; RemoveFilesEnabled = !value; this.RaiseAndSetIfChanged(ref _recurseArchivesChecked, value); } } public bool IsReady { get => _isReady; set => this.RaiseAndSetIfChanged(ref _isReady, value); } public bool ProgressVisible { get => _progressVisible; set => this.RaiseAndSetIfChanged(ref _progressVisible, value); } public string StatusMessage { get => _statusMessage; set => this.RaiseAndSetIfChanged(ref _statusMessage, value); } public double ProgressMinimum { get => _progressMinimum; set => this.RaiseAndSetIfChanged(ref _progressMinimum, value); } public double ProgressMaximum { get => _progressMaximum; set => this.RaiseAndSetIfChanged(ref _progressMaximum, value); } public double ProgressValue { get => _progressValue; 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 bool StatusMessage2Visible { get => _statusMessage2Visible; set => this.RaiseAndSetIfChanged(ref _statusMessage2Visible, value); } public string StatusMessage2 { get => _statusMessage2; set => this.RaiseAndSetIfChanged(ref _statusMessage2, 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 string FolderPath { get => _folderPath; set => this.RaiseAndSetIfChanged(ref _folderPath, value); } public bool CanClose { get => _canClose; set => this.RaiseAndSetIfChanged(ref _canClose, value); } public bool CanStart { get => _canStart; set => this.RaiseAndSetIfChanged(ref _canStart, value); } public bool IsImporting { get => _isImporting; set => this.RaiseAndSetIfChanged(ref _isImporting, value); } public ObservableCollection Importers { get; } = []; void Start() { _rootImporter = new FileImporter(_ctx, _newFiles, _newDisks, _newMedias, KnownOnlyChecked, RemoveFilesChecked); _rootImporter.SetMessage += SetMessage; _rootImporter.SetIndeterminateProgress += SetIndeterminateProgress; _rootImporter.SetProgress += SetProgress; _rootImporter.SetProgressBounds += SetProgressBounds; _rootImporter.Finished += EnumeratingFilesFinished; ProgressIsIndeterminate = true; ProgressVisible = true; CanClose = false; CanStart = false; IsImporting = true; _ = Task.Run(() => _rootImporter.FindFiles(FolderPath)); } void SetProgressBounds(object sender, ProgressBoundsEventArgs e) => Dispatcher.UIThread.Post(() => { ProgressIsIndeterminate = false; ProgressMaximum = e.Maximum; ProgressMinimum = e.Minimum; }); void SetProgress(object sender, ProgressEventArgs e) { Dispatcher.UIThread.Post(() => ProgressValue = e.Value); } void SetIndeterminateProgress(object sender, EventArgs e) { Dispatcher.UIThread.Post(() => ProgressIsIndeterminate = true); } void SetProgress2Bounds(object sender, ProgressBoundsEventArgs e) => Dispatcher.UIThread.Post(() => { Progress2IsIndeterminate = false; Progress2Maximum = e.Maximum; Progress2Minimum = e.Minimum; }); void SetProgress2(object sender, ProgressEventArgs e) { Dispatcher.UIThread.Post(() => Progress2Value = e.Value); } void SetIndeterminateProgress2(object sender, EventArgs e) { Dispatcher.UIThread.Post(() => Progress2IsIndeterminate = true); } void EnumeratingFilesFinished(object sender, EventArgs e) { _rootImporter.Finished -= EnumeratingFilesFinished; if(RecurseArchivesChecked) { Progress2Visible = true; StatusMessage2Visible = true; _rootImporter.SetMessage2 += SetMessage2; _rootImporter.SetIndeterminateProgress2 += SetIndeterminateProgress2; _rootImporter.SetProgress2 += SetProgress2; _rootImporter.SetProgressBounds2 += SetProgress2Bounds; _rootImporter.Finished += CheckArchivesFinished; _ = Task.Run(() => { _stopwatch.Restart(); _rootImporter.SeparateFilesAndArchives(); }); } else ProcessFiles(); } void ProcessFiles() { _listPosition = 0; ProgressMinimum = 0; ProgressMaximum = _rootImporter.Files.Count; ProgressValue = 0; ProgressIsIndeterminate = false; ProgressVisible = true; CanClose = false; CanStart = false; IsReady = false; IsImporting = true; _stopwatch.Restart(); Parallel.ForEach(_rootImporter.Files, file => { Dispatcher.UIThread.Post(() => { StatusMessage = string.Format(Localization.ImportingItem, Path.GetFileName(file)); ProgressValue = _listPosition; }); var model = new RomImporter { Filename = Path.GetFileName(file), Indeterminate = true }; var worker = new FileImporter(_ctx, _newFiles, _newDisks, _newMedias, KnownOnlyChecked, RemoveFilesChecked); worker.SetIndeterminateProgress2 += model.OnSetIndeterminateProgress; worker.SetMessage2 += model.OnSetMessage; worker.SetProgress2 += model.OnSetProgress; worker.SetProgressBounds2 += model.OnSetProgressBounds; worker.ImportedRom += model.OnImportedRom; worker.WorkFinished += model.OnWorkFinished; Dispatcher.UIThread.Post(() => Importers.Add(model)); worker.ImportFile(file); Interlocked.Increment(ref _listPosition); }); _stopwatch.Stop(); Console.WriteLine("Took " + _stopwatch.Elapsed.TotalSeconds + " seconds to process files."); _rootImporter.SaveChanges(); _rootImporter.UpdateRomStats(); _listPosition = 0; ProgressMinimum = 0; ProgressMaximum = 1; ProgressValue = 0; ProgressIsIndeterminate = false; ProgressVisible = false; CanClose = true; CanStart = false; IsReady = false; IsImporting = false; StatusMessage = Localization.Finished; } void ProcessArchives() { // For each archive ProgressMaximum = _rootImporter.Archives.Count; ProgressMinimum = 0; ProgressValue = 0; ProgressIsIndeterminate = false; Progress2Visible = false; StatusMessage2Visible = false; _listPosition = 0; _stopwatch.Restart(); Parallel.ForEach(_rootImporter.Archives, archive => { Dispatcher.UIThread.Post(() => { StatusMessage = "Processing archive: " + Path.GetFileName(archive); ProgressValue = _listPosition; }); // Create FileImporter var archiveImporter = new FileImporter(_ctx, _newFiles, _newDisks, _newMedias, KnownOnlyChecked, RemoveFilesChecked); // Extract archive bool ret = archiveImporter.ExtractArchive(archive); if(!ret) return; // Process files in archive foreach(string file in archiveImporter.Files) { var model = new RomImporter { Filename = Path.GetFileName(file), Indeterminate = true }; var worker = new FileImporter(_ctx, _newFiles, _newDisks, _newMedias, KnownOnlyChecked, RemoveFilesChecked); worker.SetIndeterminateProgress2 += model.OnSetIndeterminateProgress; worker.SetMessage2 += model.OnSetMessage; worker.SetProgress2 += model.OnSetProgress; worker.SetProgressBounds2 += model.OnSetProgressBounds; worker.ImportedRom += model.OnImportedRom; worker.WorkFinished += model.OnWorkFinished; Dispatcher.UIThread.Post(() => Importers.Add(model)); worker.ImportFile(file); worker.Files.Clear(); } // Remove temporary files archiveImporter.CleanupExtractedArchive(); Interlocked.Increment(ref _listPosition); }); _stopwatch.Stop(); Console.WriteLine("Took " + _stopwatch.Elapsed.TotalSeconds + " seconds to process archives."); Progress2Visible = false; StatusMessage2Visible = false; ProcessFiles(); } void CheckArchivesFinished(object sender, EventArgs e) { _stopwatch.Stop(); Console.WriteLine("Took {0} seconds to check archives.", _stopwatch.Elapsed.TotalSeconds); Progress2Visible = false; StatusMessage2Visible = false; _rootImporter.Finished -= CheckArchivesFinished; ProcessArchives(); } void SetMessage(object sender, MessageEventArgs e) { Dispatcher.UIThread.Post(() => StatusMessage = e.Message); } void SetMessage2(object sender, MessageEventArgs e) { Dispatcher.UIThread.Post(() => StatusMessage2 = e.Message); } void Close() => View.Close(); async Task SelectFolderAsync() { IReadOnlyList result = await View.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = Localization.ImportRomsFolderDialogTitle }); if(result.Count < 1) return; FolderPath = result[0].TryGetLocalPath() ?? string.Empty; IsReady = true; CanStart = true; CanClose = true; } }