From 7e6d519e5060e172e6c696ed43edfd82405565e5 Mon Sep 17 00:00:00 2001 From: Vit Nemecky Date: Fri, 1 Aug 2025 21:52:42 +0200 Subject: [PATCH 1/6] PoC - wizzard to remap missing device --- .../BusinessLogic/IPlayLogic.cs | 5 +- .../BusinessLogic/PlayLogic.cs | 34 ++++- .../UI/Pages/CreationPage.xaml | 1 + .../UI/ViewModels/CreationPageViewModel.cs | 130 +++++++++++++++++- 4 files changed, 160 insertions(+), 10 deletions(-) diff --git a/BrickController2/BrickController2/BusinessLogic/IPlayLogic.cs b/BrickController2/BrickController2/BusinessLogic/IPlayLogic.cs index 515faddc..3bb605b1 100644 --- a/BrickController2/BrickController2/BusinessLogic/IPlayLogic.cs +++ b/BrickController2/BrickController2/BusinessLogic/IPlayLogic.cs @@ -1,6 +1,6 @@ using BrickController2.CreationManagement; using BrickController2.PlatformServices.GameController; -using System.Threading.Tasks; +using System.Collections.Generic; namespace BrickController2.BusinessLogic { @@ -11,6 +11,9 @@ public interface IPlayLogic CreationValidationResult ValidateCreation(Creation creation); bool ValidateControllerAction(ControllerAction controllerAction); + IEnumerable GetMissingDevices(Creation creation); + int RemapDevice(Creation creation, string sourceDeviceId, string newDeviceId); + void StartPlay(); void StopPlay(); diff --git a/BrickController2/BrickController2/BusinessLogic/PlayLogic.cs b/BrickController2/BrickController2/BusinessLogic/PlayLogic.cs index b9604097..6e426a65 100644 --- a/BrickController2/BrickController2/BusinessLogic/PlayLogic.cs +++ b/BrickController2/BrickController2/BusinessLogic/PlayLogic.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using BrickController2.CreationManagement; +using BrickController2.CreationManagement; using BrickController2.DeviceManagement; using BrickController2.PlatformServices.GameController; +using System; +using System.Collections.Generic; +using System.Linq; namespace BrickController2.BusinessLogic { @@ -59,6 +59,32 @@ public bool ValidateControllerAction(ControllerAction controllerAction) return device != null && (controllerAction.ButtonType != ControllerButtonType.Sequence || sequence != null); } + public IEnumerable GetMissingDevices(Creation creation) + { + return creation.GetDeviceIds().Where(d => _deviceManager.GetDeviceById(d) == null); + } + + public int RemapDevice(Creation creation, string sourceDeviceId, string newDeviceId) + { + var counter = 0; + foreach (var profile in creation.ControllerProfiles) + { + foreach (var controllerEvent in profile.ControllerEvents) + { + foreach (var controllerAction in controllerEvent.ControllerActions) + { + if (controllerAction.DeviceId == sourceDeviceId) + { + controllerAction.DeviceId = newDeviceId; + counter++; + } + } + } + } + + return counter; + } + public void StartPlay() { _sequencePlayer.StartPlayer(); diff --git a/BrickController2/BrickController2/UI/Pages/CreationPage.xaml b/BrickController2/BrickController2/UI/Pages/CreationPage.xaml index 5cf82571..f9a88263 100644 --- a/BrickController2/BrickController2/UI/Pages/CreationPage.xaml +++ b/BrickController2/BrickController2/UI/Pages/CreationPage.xaml @@ -36,6 +36,7 @@ + diff --git a/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs b/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs index 6ffe3140..422b7313 100644 --- a/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs +++ b/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs @@ -1,16 +1,18 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using System.Windows.Input; -using BrickController2.BusinessLogic; +using BrickController2.BusinessLogic; using BrickController2.CreationManagement; using BrickController2.CreationManagement.Sharing; +using BrickController2.DeviceManagement; using BrickController2.Helpers; using BrickController2.PlatformServices.SharedFileStorage; using BrickController2.UI.Commands; using BrickController2.UI.Services.Dialog; using BrickController2.UI.Services.Navigation; +using BrickController2.UI.Services.Theme; using BrickController2.UI.Services.Translation; +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Input; namespace BrickController2.UI.ViewModels { @@ -20,6 +22,7 @@ public class CreationPageViewModel : PageViewModelBase private readonly IDialogService _dialogService; private readonly IPlayLogic _playLogic; private readonly ISharingManager _sharingManagerProfile; + private readonly IDeviceManager _deviceManager; public CreationPageViewModel( INavigationService navigationService, @@ -30,6 +33,7 @@ public CreationPageViewModel( IPlayLogic playLogic, ISharingManager sharingManagerProfile, ICommandFactory commandFactory, + IDeviceManager deviceManager, NavigationParameters parameters) : base(navigationService, translationService) { @@ -38,6 +42,7 @@ public CreationPageViewModel( SharedFileStorageService = sharedFileStorageService; _playLogic = playLogic; _sharingManagerProfile = sharingManagerProfile; + _deviceManager = deviceManager; Creation = parameters.Get("creation"); ImportControllerProfileCommand = new SafeCommand(async () => await ImportControllerProfileAsync(), () => SharedFileStorageService.IsSharedStorageAvailable); @@ -49,6 +54,7 @@ public CreationPageViewModel( ShareCreationCommand = new SafeCommand(ShareCreationAsync); ShareCreationAsFileCommand = commandFactory.ShareAsJsonFileCommand(this, Creation); PlayCommand = new SafeCommand(async () => await PlayAsync()); + FixItCommand = new SafeCommand(async () => await FixItAsync()); AddControllerProfileCommand = new SafeCommand(async () => await AddControllerProfileAsync()); ControllerProfileTappedCommand = new SafeCommand(async controllerProfile => await NavigationService.NavigateToAsync(new NavigationParameters(("controllerprofile", controllerProfile)))); DeleteControllerProfileCommand = new SafeCommand(async controllerProfile => await DeleteControllerProfileAsync(controllerProfile)); @@ -69,6 +75,7 @@ public CreationPageViewModel( public ICommand ShareCreationAsFileCommand { get; } public ICommand RenameCreationCommand { get; } public ICommand PlayCommand { get; } + public ICommand FixItCommand { get; } public ICommand AddControllerProfileCommand { get; } public ICommand ControllerProfileTappedCommand { get; } public ICommand DeleteControllerProfileCommand { get; } @@ -152,6 +159,119 @@ await _dialogService.ShowMessageBoxAsync( } } + private readonly struct DeviceModel(Device device) + { + public string DeviceId => device.Id; + public override string ToString() + { + return string.IsNullOrWhiteSpace(device.Name) ? device.Address : device.Name; + } + } + + private async Task FixItAsync() + { + try + { + var validationResult = _playLogic.ValidateCreation(Creation); + + if (validationResult == CreationValidationResult.MissingDevice) + { + // get missing device IDs + var deviceIds = _playLogic.GetMissingDevices(Creation) + .Select(id => + { + DeviceId.TryParse(id, out var deviceType, out var deviceAddress); + return (DeviceType: deviceType, Address: deviceAddress); + }) + .Where(x => x.DeviceType != DeviceType.Unknown && x.Address != null) + .ToList(); + + // get missing types + var missingTypes = deviceIds.Select(x => x.DeviceType).ToHashSet(); + + if (missingTypes.Count == 0 || _deviceManager.Devices.All(x => !missingTypes.Contains(x.DeviceType))) + { + // report error - no suitable missing devices + return; + } + + DeviceType deviceType; + + if (missingTypes.Count == 1) + { + deviceType = missingTypes.First(); + } + else + { + // choose device type to remap + var sourceType = await _dialogService.ShowSelectionDialogAsync( + missingTypes.Select(x => x.ToString()), + Translate("Missing device type"), + Translate("Cancel"), + DisappearingToken); + + if (!sourceType.IsOk || !Enum.TryParse(sourceType.SelectedItem, out deviceType)) + { + return; // user cancelled or invalid type + } + } + + // have device type + var missingDeviceAddresses = deviceIds + .Where(x => x.DeviceType == deviceType) + .Select(x => x.Address!) + .ToList(); + + string missingDeviceAddress = missingDeviceAddresses[0]; + if (missingDeviceAddresses.Count > 1) + { + var sourceDevice = await _dialogService.ShowSelectionDialogAsync( + missingDeviceAddresses, + Translate("Missing device"), + Translate("Cancel"), + DisappearingToken); + + if (!sourceDevice.IsOk) + { + return; // user cancelled + } + + missingDeviceAddress = sourceDevice.SelectedItem!; + } + + var suitableDevices = _deviceManager.Devices + .Where(d => d.DeviceType == deviceType) + .Select(d => new DeviceModel(d)) + .ToList(); + + var targetDevice = await _dialogService.ShowSelectionDialogAsync( + suitableDevices, + Translate("Target device"), + Translate("Cancel"), + DisappearingToken); + + if (targetDevice.IsOk) + { + // replace all missing device IDs with the selected one + var missingDeviceId = DeviceId.Get(deviceType, missingDeviceAddress); + var newDeviceId = targetDevice.SelectedItem!.DeviceId; + var count = _playLogic.RemapDevice(Creation, missingDeviceId, newDeviceId); + + //TODO update creation + + await _dialogService.ShowMessageBoxAsync( + Translate("Information"), + Translate($"Remapped {count} controller actions from device '{missingDeviceId}' to '{newDeviceId}'"), + Translate("Ok"), + DisappearingToken); + } + } + } + catch (OperationCanceledException) + { + } + } + private async Task AddControllerProfileAsync() { try From 48563c7dcdd8bec9f107a8fcc3b866439cb74fc6 Mon Sep 17 00:00:00 2001 From: Vit Nemecky Date: Tue, 5 Aug 2025 22:30:50 +0200 Subject: [PATCH 2/6] finetune workflow --- .../BusinessLogic/IPlayLogic.cs | 1 - .../BusinessLogic/PlayLogic.cs | 21 ---- .../CreationManagement/ControllerAction.cs | 12 ++ .../CreationManagement/CreationManager.cs | 24 ++++ .../CreationManagement/ICreationManager.cs | 1 + .../UI/ViewModels/CreationPageViewModel.cs | 103 +++++++++++------- 6 files changed, 101 insertions(+), 61 deletions(-) diff --git a/BrickController2/BrickController2/BusinessLogic/IPlayLogic.cs b/BrickController2/BrickController2/BusinessLogic/IPlayLogic.cs index 3bb605b1..15851ec9 100644 --- a/BrickController2/BrickController2/BusinessLogic/IPlayLogic.cs +++ b/BrickController2/BrickController2/BusinessLogic/IPlayLogic.cs @@ -12,7 +12,6 @@ public interface IPlayLogic bool ValidateControllerAction(ControllerAction controllerAction); IEnumerable GetMissingDevices(Creation creation); - int RemapDevice(Creation creation, string sourceDeviceId, string newDeviceId); void StartPlay(); void StopPlay(); diff --git a/BrickController2/BrickController2/BusinessLogic/PlayLogic.cs b/BrickController2/BrickController2/BusinessLogic/PlayLogic.cs index 6e426a65..ebd920c9 100644 --- a/BrickController2/BrickController2/BusinessLogic/PlayLogic.cs +++ b/BrickController2/BrickController2/BusinessLogic/PlayLogic.cs @@ -64,27 +64,6 @@ public IEnumerable GetMissingDevices(Creation creation) return creation.GetDeviceIds().Where(d => _deviceManager.GetDeviceById(d) == null); } - public int RemapDevice(Creation creation, string sourceDeviceId, string newDeviceId) - { - var counter = 0; - foreach (var profile in creation.ControllerProfiles) - { - foreach (var controllerEvent in profile.ControllerEvents) - { - foreach (var controllerAction in controllerEvent.ControllerActions) - { - if (controllerAction.DeviceId == sourceDeviceId) - { - controllerAction.DeviceId = newDeviceId; - counter++; - } - } - } - } - - return counter; - } - public void StartPlay() { _sequencePlayer.StartPlayer(); diff --git a/BrickController2/BrickController2/CreationManagement/ControllerAction.cs b/BrickController2/BrickController2/CreationManagement/ControllerAction.cs index 9b6404a3..43ca7ac7 100644 --- a/BrickController2/BrickController2/CreationManagement/ControllerAction.cs +++ b/BrickController2/BrickController2/CreationManagement/ControllerAction.cs @@ -122,5 +122,17 @@ public override string ToString() { return $"{DeviceId} - {Channel}"; } + + public bool RemapDevice(string sourceDeviceId, string newDeviceId) + { + // check action to remap the device ID + if (DeviceId == sourceDeviceId) + { + DeviceId = newDeviceId; + return true; + } + + return false; + } } } diff --git a/BrickController2/BrickController2/CreationManagement/CreationManager.cs b/BrickController2/BrickController2/CreationManagement/CreationManager.cs index 17481cd4..3b7e94e3 100644 --- a/BrickController2/BrickController2/CreationManagement/CreationManager.cs +++ b/BrickController2/BrickController2/CreationManagement/CreationManager.cs @@ -128,6 +128,30 @@ public async Task RenameCreationAsync(Creation creation, string newName) } } + public async Task RemapDevice(Creation creation, string sourceDeviceId, string newDeviceId) + { + using (await _asyncLock.LockAsync()) + { + var counter = 0; + foreach (var profile in creation.ControllerProfiles) + { + foreach (var controllerEvent in profile.ControllerEvents) + { + foreach (var controllerAction in controllerEvent.ControllerActions) + { + // if remapping is applied, persist the change + if (controllerAction.RemapDevice(sourceDeviceId, newDeviceId)) + { + await _creationRepository.UpdateControllerActionAsync(controllerAction); + counter++; + } + } + } + } + return counter; + } + } + public async Task IsControllerProfileNameAvailableAsync(Creation creation, string controllerProfileName) { using (await _asyncLock.LockAsync()) diff --git a/BrickController2/BrickController2/CreationManagement/ICreationManager.cs b/BrickController2/BrickController2/CreationManagement/ICreationManager.cs index 91ed5fd7..734d0690 100644 --- a/BrickController2/BrickController2/CreationManagement/ICreationManager.cs +++ b/BrickController2/BrickController2/CreationManagement/ICreationManager.cs @@ -18,6 +18,7 @@ public interface ICreationManager Task AddCreationAsync(string creationName); Task DeleteCreationAsync(Creation creation); Task RenameCreationAsync(Creation creation, string newName); + Task RemapDevice(Creation creation, string sourceDeviceId, string newDeviceId); Task ImportControllerProfileAsync(Creation creation, string controllerProfileFilename); Task ImportControllerProfileAsync(Creation creation, ControllerProfile controllerProfile); diff --git a/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs b/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs index 422b7313..46191c04 100644 --- a/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs +++ b/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs @@ -188,59 +188,44 @@ private async Task FixItAsync() // get missing types var missingTypes = deviceIds.Select(x => x.DeviceType).ToHashSet(); + // missing types that have some existing device of such type present + var suitableTypes = missingTypes.Where(x => _deviceManager.Devices.Any(d => d.DeviceType == x)) + .ToHashSet(); - if (missingTypes.Count == 0 || _deviceManager.Devices.All(x => !missingTypes.Contains(x.DeviceType))) + if (missingTypes.Count == 0 || suitableTypes.Count == 0) { // report error - no suitable missing devices + await _dialogService.ShowMessageBoxAsync( + Translate("Information"), + Translate("No suitable device found to replace missing device."), + Translate("Ok"), + DisappearingToken); return; } - DeviceType deviceType; - - if (missingTypes.Count == 1) + var sourceType = await ChooseDeviceTypeToRemapAsync(suitableTypes); + if (sourceType is null) { - deviceType = missingTypes.First(); - } - else - { - // choose device type to remap - var sourceType = await _dialogService.ShowSelectionDialogAsync( - missingTypes.Select(x => x.ToString()), - Translate("Missing device type"), - Translate("Cancel"), - DisappearingToken); - - if (!sourceType.IsOk || !Enum.TryParse(sourceType.SelectedItem, out deviceType)) - { - return; // user cancelled or invalid type - } + // user cancelled + return; } // have device type var missingDeviceAddresses = deviceIds - .Where(x => x.DeviceType == deviceType) + .Where(x => x.DeviceType == sourceType.Value) .Select(x => x.Address!) .ToList(); - string missingDeviceAddress = missingDeviceAddresses[0]; - if (missingDeviceAddresses.Count > 1) + var sourceDeviceAddress = await ChooseDeviceAddressToRemapAsync(missingDeviceAddresses); + if (sourceDeviceAddress is null) { - var sourceDevice = await _dialogService.ShowSelectionDialogAsync( - missingDeviceAddresses, - Translate("Missing device"), - Translate("Cancel"), - DisappearingToken); - - if (!sourceDevice.IsOk) - { - return; // user cancelled - } - - missingDeviceAddress = sourceDevice.SelectedItem!; + // user cancelled + return; } + // choose target device by name var suitableDevices = _deviceManager.Devices - .Where(d => d.DeviceType == deviceType) + .Where(d => d.DeviceType == sourceType.Value) .Select(d => new DeviceModel(d)) .ToList(); @@ -253,11 +238,9 @@ private async Task FixItAsync() if (targetDevice.IsOk) { // replace all missing device IDs with the selected one - var missingDeviceId = DeviceId.Get(deviceType, missingDeviceAddress); + var missingDeviceId = DeviceId.Get(sourceType.Value, sourceDeviceAddress); var newDeviceId = targetDevice.SelectedItem!.DeviceId; - var count = _playLogic.RemapDevice(Creation, missingDeviceId, newDeviceId); - - //TODO update creation + var count = await _creationManager.RemapDevice(Creation, missingDeviceId, newDeviceId); await _dialogService.ShowMessageBoxAsync( Translate("Information"), @@ -272,6 +255,48 @@ await _dialogService.ShowMessageBoxAsync( } } + private async Task ChooseDeviceAddressToRemapAsync(System.Collections.Generic.List missingDeviceAddresses) + { + if (missingDeviceAddresses.Count == 1) + { + return missingDeviceAddresses[0]; + } + + // choose address to remap + var sourceDevice = await _dialogService.ShowSelectionDialogAsync( + missingDeviceAddresses, + Translate("Missing device"), + Translate("Cancel"), + DisappearingToken); + + if (sourceDevice.IsOk) + { + return sourceDevice.SelectedItem; + } + return default; + } + + private async Task ChooseDeviceTypeToRemapAsync(System.Collections.Generic.HashSet suitableTypes) + { + if (suitableTypes.Count == 1) + { + return suitableTypes.First(); + } + + // choose device type to remap + var deviceType = await _dialogService.ShowSelectionDialogAsync( + suitableTypes, + Translate("Missing device type"), + Translate("Cancel"), + DisappearingToken); + + if (deviceType.IsOk) + { + return deviceType.SelectedItem; + } + return default; + } + private async Task AddControllerProfileAsync() { try From 72560a3abbd01245cf81090dd5ceafe2d301b613 Mon Sep 17 00:00:00 2001 From: Vit Nemecky Date: Mon, 11 Aug 2025 21:26:32 +0200 Subject: [PATCH 3/6] apply validity to show / hide Fix / Play for creation --- .../UI/Pages/CreationPage.xaml | 11 ++++-- .../UI/ViewModels/CreationPageViewModel.cs | 35 +++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/BrickController2/BrickController2/UI/Pages/CreationPage.xaml b/BrickController2/BrickController2/UI/Pages/CreationPage.xaml index f9a88263..be77723b 100644 --- a/BrickController2/BrickController2/UI/Pages/CreationPage.xaml +++ b/BrickController2/BrickController2/UI/Pages/CreationPage.xaml @@ -3,6 +3,7 @@ xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:controls="clr-namespace:BrickController2.UI.Controls" + xmlns:converters="clr-namespace:BrickController2.UI.Converters" xmlns:extensions="clr-namespace:BrickController2.UI.MarkupExtensions" xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls" xmlns:local="clr-namespace:BrickController2.UI.Pages" @@ -12,6 +13,12 @@ ios:Page.UseSafeArea="True" BackgroundColor="{DynamicResource PageBackgroundColor}"> + + + + + + @@ -36,8 +43,8 @@ - - + + diff --git a/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs b/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs index 46191c04..3349b465 100644 --- a/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs +++ b/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs @@ -7,7 +7,6 @@ using BrickController2.UI.Commands; using BrickController2.UI.Services.Dialog; using BrickController2.UI.Services.Navigation; -using BrickController2.UI.Services.Theme; using BrickController2.UI.Services.Translation; using System; using System.Linq; @@ -65,6 +64,8 @@ public CreationPageViewModel( public bool HasMultipleControllerProfiles => Creation.ControllerProfiles.Count > 1; + public bool IsCreationValid => _playLogic.ValidateCreation(Creation) == CreationValidationResult.Ok; + public ISharedFileStorageService SharedFileStorageService { get; } public ICommand ImportControllerProfileCommand { get; } public ICommand CopyControllerProfileCommand { get; } @@ -81,6 +82,24 @@ public CreationPageViewModel( public ICommand DeleteControllerProfileCommand { get; } public ICommand PlayControllerProfileCommand { get; } + public override void OnAppearing() + { + base.OnAppearing(); + // recheck creation validity + RecheckCreationValidity(); + } + + private void OnProfilesCountChanged() + { + RaisePropertyChanged(nameof(HasMultipleControllerProfiles)); + } + + private void RecheckCreationValidity() + { + // recheck validity of the creation + RaisePropertyChanged(nameof(IsCreationValid)); + } + private async Task RenameCreationAsync() { try @@ -247,6 +266,8 @@ await _dialogService.ShowMessageBoxAsync( Translate($"Remapped {count} controller actions from device '{missingDeviceId}' to '{newDeviceId}'"), Translate("Ok"), DisappearingToken); + + RecheckCreationValidity(); } } } @@ -330,7 +351,8 @@ await _dialogService.ShowProgressDialogAsync( Translate("Creating"), token: DisappearingToken); // notify profile count change - RaisePropertyChanged(nameof(HasMultipleControllerProfiles)); + OnProfilesCountChanged(); + RecheckCreationValidity(); await NavigationService.NavigateToAsync(new NavigationParameters(("controllerprofile", controllerProfile!))); } @@ -357,7 +379,8 @@ await _dialogService.ShowProgressDialogAsync( Translate("Deleting"), token: DisappearingToken); // notify profile count change - RaisePropertyChanged(nameof(HasMultipleControllerProfiles)); + OnProfilesCountChanged(); + RecheckCreationValidity(); } } catch (OperationCanceledException) @@ -383,6 +406,9 @@ private async Task ImportControllerProfileAsync() try { await _creationManager.ImportControllerProfileAsync(Creation, controllerProfileFilesMap[result.SelectedItem]); + // notify profile count change + OnProfilesCountChanged(); + RecheckCreationValidity(); } catch (Exception) { @@ -413,6 +439,9 @@ private async Task PasteControllerProfileAsync() { var profile = await _sharingManagerProfile.ImportFromClipboardAsync(); await _creationManager.ImportControllerProfileAsync(Creation, profile); + // notify profile count change + OnProfilesCountChanged(); + RecheckCreationValidity(); } catch (Exception ex) { From 2ae3e7aab741b7512878ca4534253abf5a0f6463 Mon Sep 17 00:00:00 2001 From: Vit Nemecky Date: Tue, 12 Aug 2025 22:08:33 +0200 Subject: [PATCH 4/6] command factory + Fix enablement --- .../CreationManagement/Creation.cs | 21 +- .../UI/Commands/CreationCommandFactory.cs | 218 +++++++++++++++- .../UI/Commands/ICreationCommandFactory.cs | 16 ++ .../BrickController2/UI/DI/UiModule.cs | 2 +- .../ControllerProfilePageViewModel.cs | 39 +-- .../ViewModels/CreationListPageViewModel.cs | 49 +--- .../UI/ViewModels/CreationPageViewModel.cs | 238 +++--------------- 7 files changed, 291 insertions(+), 292 deletions(-) create mode 100644 BrickController2/BrickController2/UI/Commands/ICreationCommandFactory.cs diff --git a/BrickController2/BrickController2/CreationManagement/Creation.cs b/BrickController2/BrickController2/CreationManagement/Creation.cs index 9ecb6101..6c25bc69 100644 --- a/BrickController2/BrickController2/CreationManagement/Creation.cs +++ b/BrickController2/BrickController2/CreationManagement/Creation.cs @@ -1,4 +1,5 @@ -using BrickController2.CreationManagement.Sharing; +using BrickController2.BusinessLogic; +using BrickController2.CreationManagement.Sharing; using BrickController2.Helpers; using Newtonsoft.Json; using SQLite; @@ -12,6 +13,7 @@ public class Creation : NotifyPropertyChangedSource, IShareable { private string _name = string.Empty; private ObservableCollection _controllerProfiles = new ObservableCollection(); + private CreationValidationResult _lastValidation; [PrimaryKey, AutoIncrement] [JsonIgnore] @@ -30,6 +32,23 @@ public ObservableCollection ControllerProfiles set { _controllerProfiles = value; RaisePropertyChanged(); } } + /// + /// Keeps track of the last validation result for this creation. + /// + [JsonIgnore] + public CreationValidationResult ValidationResult + { + get { return _lastValidation; } + set + { + if (_lastValidation != value) + { + _lastValidation = value; + RaisePropertyChanged(); + } + } + } + [JsonIgnore] public static string Type => "bc2c"; diff --git a/BrickController2/BrickController2/UI/Commands/CreationCommandFactory.cs b/BrickController2/BrickController2/UI/Commands/CreationCommandFactory.cs index eee2a32c..59e120f5 100644 --- a/BrickController2/BrickController2/UI/Commands/CreationCommandFactory.cs +++ b/BrickController2/BrickController2/UI/Commands/CreationCommandFactory.cs @@ -1,17 +1,25 @@ -using BrickController2.CreationManagement; +using BrickController2.BusinessLogic; +using BrickController2.CreationManagement; using BrickController2.CreationManagement.Sharing; +using BrickController2.DeviceManagement; using BrickController2.PlatformServices.SharedFileStorage; using BrickController2.UI.Services.Dialog; using BrickController2.UI.Services.Navigation; using BrickController2.UI.Services.Translation; +using BrickController2.UI.ViewModels; using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using System.Windows.Input; namespace BrickController2.UI.Commands; -internal class CreationCommandFactory : ItemCommandFactoryBase, ICommandFactory +internal class CreationCommandFactory : ItemCommandFactoryBase, ICommandFactory, ICreationCommandFactory { private readonly ICreationManager _creationManager; + private readonly IDeviceManager _deviceManager; + private readonly IPlayLogic _playLogic; public CreationCommandFactory ( @@ -20,12 +28,28 @@ public CreationCommandFactory ISharingManager sharingManager, ISharedFileStorageService sharedFileStorageService, INavigationService navigationService, - ICreationManager creationManager + ICreationManager creationManager, + IDeviceManager deviceManager, + IPlayLogic playLogic ) : base(dialogService, translationService, sharingManager, sharedFileStorageService, navigationService) { _creationManager = creationManager; + _deviceManager = deviceManager; + _playLogic = playLogic; } + public ICommand PlayCommand(PageViewModelBase viewModel, Creation creation, ControllerProfile? controllerProfile = default!) + => new SafeCommand(() => PlayAsync(viewModel, creation, controllerProfile)); + + public ICommand PlayCreationCommand(PageViewModelBase viewModel) + => new SafeCommand((creation) => PlayAsync(viewModel, creation)); + + public ICommand PlayControllerProfileCommand(PageViewModelBase viewModel) + => new SafeCommand((profile) => PlayAsync(viewModel, profile.Creation, profile)); + + public ICommand FixCommand(PageViewModelBase viewModel, Creation creation) + => new SafeCommand(() => FixItAsync(viewModel, creation)); + protected override string ItemsTitle => Translate("Creations"); protected override string ItemNameHint => Translate("CreationName"); protected override string NoItemToImportMessage => Translate("NoCreationsToImport"); @@ -37,4 +61,192 @@ protected override Task ExportItemAsync(Creation model, string fileName) protected override Task ImportItemAsync(Creation model) => _creationManager.ImportCreationAsync(model); + + private async Task PlayAsync(PageViewModelBase viewModel, Creation creation, ControllerProfile? controllerProfile = default!) + { + try + { + var validationResult = _playLogic.ValidateCreation(creation); + + string warning = string.Empty; + switch (validationResult) + { + case CreationValidationResult.MissingControllerAction: + warning = Translate("NoControllerActions"); + break; + + case CreationValidationResult.MissingDevice: + warning = Translate("MissingDevices"); + break; + + case CreationValidationResult.MissingSequence: + warning = Translate("MissingSequence"); + break; + } + + if (validationResult == CreationValidationResult.Ok) + { + await NavigationService.NavigateToAsync(new NavigationParameters( + ("creation", creation), + ("profile", controllerProfile))); + } + else + { + await DialogService.ShowMessageBoxAsync( + Translate("Warning"), + warning, + Translate("Ok"), + viewModel.DisappearingToken); + } + } + catch (OperationCanceledException) + { + } + } + + private async Task FixItAsync(PageViewModelBase viewModel, Creation creation) + { + try + { + var validationResult = _playLogic.ValidateCreation(creation); + + if (validationResult == CreationValidationResult.MissingDevice) + { + await FixMissingDevicesAsync(viewModel, creation); + } + else if (validationResult == CreationValidationResult.MissingSequence) + { + await DialogService.ShowMessageBoxAsync( + Translate("Warning"), + Translate("MissingSequence"), + Translate("Ok"), + viewModel.DisappearingToken); + } + else if (validationResult == CreationValidationResult.MissingControllerAction) + { + await DialogService.ShowMessageBoxAsync( + Translate("Warning"), + Translate("NoControllerActions"), + Translate("Ok"), + viewModel.DisappearingToken); + } + } + catch (OperationCanceledException) + { + } + } + + private async Task FixMissingDevicesAsync(PageViewModelBase viewModel, Creation creation) + { + // get missing device IDs + var deviceIds = _playLogic.GetMissingDevices(creation) + .Select(id => + { + DeviceId.TryParse(id, out var deviceType, out var deviceAddress); + return (DeviceType: deviceType, Address: deviceAddress); + }) + .Where(x => x.DeviceType != DeviceType.Unknown && x.Address != null) + .ToList(); + + // get missing types + var missingTypes = deviceIds.Select(x => x.DeviceType).ToHashSet(); + // missing types that have some existing device of such type present + var suitableTypes = missingTypes.Where(x => _deviceManager.Devices.Any(d => d.DeviceType == x)) + .ToHashSet(); + + if (missingTypes.Count == 0 || suitableTypes.Count == 0) + { + // report error - no suitable missing devices + await DialogService.ShowMessageBoxAsync( + Translate("Information"), + Translate("No suitable device found to replace missing device."), + Translate("Ok"), + viewModel.DisappearingToken); + return false; + } + + var sourceType = await ChooseDeviceTypeToRemapAsync(viewModel, suitableTypes); + if (sourceType is null) + { + // user cancelled + return false; + } + + // have device type + var missingDeviceAddresses = deviceIds + .Where(x => x.DeviceType == sourceType.Value) + .Select(x => x.Address!) + .ToList(); + + var sourceDeviceAddress = await ChooseDeviceAddressToRemapAsync(viewModel, missingDeviceAddresses); + if (sourceDeviceAddress is null) + { + // user cancelled + return false; + } + + // choose target device by name + var suitableDevices = _deviceManager.Devices + .Where(d => d.DeviceType == sourceType.Value) + .ToList(); + + var targetDevice = await DialogService.ShowSelectionDialogAsync( + suitableDevices, + Translate("Target device"), + Translate("Cancel"), + viewModel.DisappearingToken); + + if (targetDevice.IsOk) + { + // replace all missing device IDs with the selected one + var missingDeviceId = DeviceId.Get(sourceType.Value, sourceDeviceAddress); + var newDeviceId = targetDevice.SelectedItem!.Id; + var count = await _creationManager.RemapDevice(creation, missingDeviceId, newDeviceId); + + // revalidate creation + creation.ValidationResult = _playLogic.ValidateCreation(creation); + + await DialogService.ShowMessageBoxAsync( + Translate("Information"), + Translate($"Remapped {count} controller actions from device '{missingDeviceId}' to '{newDeviceId}'"), + Translate("Ok"), + viewModel.DisappearingToken); + } + + return true; + } + + private async Task ChooseDeviceAddressToRemapAsync(PageViewModelBase viewModel, List missingDeviceAddresses) + { + if (missingDeviceAddresses.Count == 1) + { + return missingDeviceAddresses[0]; + } + + // choose address to remap + var sourceDevice = await DialogService.ShowSelectionDialogAsync( + missingDeviceAddresses, + Translate("Missing device"), + Translate("Cancel"), + viewModel.DisappearingToken); + + return sourceDevice.IsOk ? sourceDevice.SelectedItem : default; + } + + private async Task ChooseDeviceTypeToRemapAsync(PageViewModelBase viewModel, HashSet suitableTypes) + { + if (suitableTypes.Count == 1) + { + return suitableTypes.First(); + } + + // choose device type to remap + var deviceType = await DialogService.ShowSelectionDialogAsync( + suitableTypes, + Translate("Missing device type"), + Translate("Cancel"), + viewModel.DisappearingToken); + + return deviceType.IsOk ? deviceType.SelectedItem : default; + } } diff --git a/BrickController2/BrickController2/UI/Commands/ICreationCommandFactory.cs b/BrickController2/BrickController2/UI/Commands/ICreationCommandFactory.cs new file mode 100644 index 00000000..6d589216 --- /dev/null +++ b/BrickController2/BrickController2/UI/Commands/ICreationCommandFactory.cs @@ -0,0 +1,16 @@ +using BrickController2.CreationManagement; +using BrickController2.UI.ViewModels; +using System.Windows.Input; + +namespace BrickController2.UI.Commands; + +public interface ICreationCommandFactory : ICommandFactory +{ + ICommand PlayCommand(PageViewModelBase viewModel, Creation creation, ControllerProfile? controllerProfile = default!); + + ICommand PlayCreationCommand(PageViewModelBase viewModel); + + ICommand PlayControllerProfileCommand(PageViewModelBase viewModel); + + ICommand FixCommand(PageViewModelBase viewModel, Creation creation); +} diff --git a/BrickController2/BrickController2/UI/DI/UiModule.cs b/BrickController2/BrickController2/UI/DI/UiModule.cs index bfbf4792..be087fa2 100644 --- a/BrickController2/BrickController2/UI/DI/UiModule.cs +++ b/BrickController2/BrickController2/UI/DI/UiModule.cs @@ -62,7 +62,7 @@ protected override void Load(ContainerBuilder builder) }); // command related registration - builder.RegisterType().As>().SingleInstance(); + builder.RegisterType().As>().As().SingleInstance(); builder.RegisterType().As>().SingleInstance(); // Xamarin forms related diff --git a/BrickController2/BrickController2/UI/ViewModels/ControllerProfilePageViewModel.cs b/BrickController2/BrickController2/UI/ViewModels/ControllerProfilePageViewModel.cs index 49b23f37..440c396e 100644 --- a/BrickController2/BrickController2/UI/ViewModels/ControllerProfilePageViewModel.cs +++ b/BrickController2/BrickController2/UI/ViewModels/ControllerProfilePageViewModel.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using BrickController2.CreationManagement; @@ -40,6 +39,7 @@ public ControllerProfilePageViewModel( IDialogService dialogService, ISharedFileStorageService sharedFileStorageService, IPlayLogic playLogic, + ICreationCommandFactory commandFactory, IGameControllerService gameControllerService, NavigationParameters parameters) : base(navigationService, translationService) @@ -59,7 +59,7 @@ public ControllerProfilePageViewModel( RenameProfileCommand = new SafeCommand(async () => await RenameControllerProfileAsync()); AddControllerEventCommand = new SafeCommand(async () => await AddControllerEventAsync(false)); AddControllerEventForSpecificControllerIdCommand = new SafeCommand(async () => await AddControllerEventAsync(true)); - PlayCommand = new SafeCommand(async () => await PlayAsync()); + PlayCommand = commandFactory.PlayCommand(this, ControllerProfile.Creation!, ControllerProfile); ControllerActionTappedCommand = new SafeCommand(ShowActionAsync); DeleteControllerEventCommand = new SafeCommand(async controllerEvent => await DeleteControllerEventAsync(controllerEvent)); AddAnotherActionCommand = new SafeCommand(AddAnotherActionAsync); @@ -267,41 +267,6 @@ await _dialogService.ShowMessageBoxAsync( } } - private async Task PlayAsync() - { - var validationResult = _playLogic.ValidateCreation(ControllerProfile.Creation!); - - string warning = string.Empty; - switch (validationResult) - { - case CreationValidationResult.MissingControllerAction: - warning = Translate("NoControllerActions"); - break; - - case CreationValidationResult.MissingDevice: - warning = Translate("MissingDevices"); - break; - - case CreationValidationResult.MissingSequence: - warning = Translate("MissingSequence"); - break; - } - - if (validationResult == CreationValidationResult.Ok) - { - await NavigationService.NavigateToAsync(new NavigationParameters( - ("creation", ControllerProfile.Creation!), - ("profile", ControllerProfile))); - } - else - { - await _dialogService.ShowMessageBoxAsync( - Translate("Warning"), - warning, - Translate("Ok"), - DisappearingToken); - } - } private async Task AddAnotherActionAsync(ControllerEvent controllerEvent) { try diff --git a/BrickController2/BrickController2/UI/ViewModels/CreationListPageViewModel.cs b/BrickController2/BrickController2/UI/ViewModels/CreationListPageViewModel.cs index 357d99c1..4e2db2d2 100644 --- a/BrickController2/BrickController2/UI/ViewModels/CreationListPageViewModel.cs +++ b/BrickController2/BrickController2/UI/ViewModels/CreationListPageViewModel.cs @@ -41,7 +41,7 @@ public CreationListPageViewModel( IPlayLogic playLogic, IDialogService dialogService, ISharedFileStorageService sharedFileStorageService, - ICommandFactory commandFactory, + ICreationCommandFactory commandFactory, IBluetoothPermission bluetoothPermission, IReadWriteExternalStoragePermission readWriteExternalStoragePermission) : base(navigationService, translationService) @@ -62,7 +62,7 @@ public CreationListPageViewModel( AddCreationCommand = new SafeCommand(async () => await AddCreationAsync()); CreationTappedCommand = new SafeCommand(async creation => await NavigationService.NavigateToAsync(new NavigationParameters(("creation", creation)))); DeleteCreationCommand = new SafeCommand(async creation => await DeleteCreationAsync(creation)); - PlayCreationCommand = new SafeCommand(PlayAsync); + PlayCreationCommand = commandFactory.PlayCreationCommand(this); ShareCreationCommand = new SafeCommand(async creation => await NavigationService.NavigateToAsync(new NavigationParameters(("item", creation)))); NavigateToDevicesCommand = new SafeCommand(async () => await NavigationService.NavigateToAsync()); NavigateToControllerTesterCommand = new SafeCommand(async () => await NavigationService.NavigateToAsync()); @@ -98,6 +98,11 @@ public override async void OnAppearing() await LoadCreationsAndDevicesAsync(); await RequestPermissionsAsync(); } + // update validation statuses + foreach (var creation in _creationManager.Creations) + { + creation.ValidationResult = _playLogic.ValidateCreation(creation); + } } public override void OnDisappearing() @@ -260,45 +265,5 @@ await _dialogService.ShowProgressDialogAsync( { } } - - private async Task PlayAsync(Creation creation) - { - try - { - var validationResult = _playLogic.ValidateCreation(creation); - - string warning = string.Empty; - switch (validationResult) - { - case CreationValidationResult.MissingControllerAction: - warning = Translate("NoControllerActions"); - break; - - case CreationValidationResult.MissingDevice: - warning = Translate("MissingDevices"); - break; - - case CreationValidationResult.MissingSequence: - warning = Translate("MissingSequence"); - break; - } - - if (validationResult == CreationValidationResult.Ok) - { - await NavigationService.NavigateToAsync(new NavigationParameters(("creation", creation))); - } - else - { - await _dialogService.ShowMessageBoxAsync( - Translate("Warning"), - Translate("Play") + $" '{creation.Name}': {warning}", - Translate("Ok"), - DisappearingToken); - } - } - catch (OperationCanceledException) - { - } - } } } \ No newline at end of file diff --git a/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs b/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs index 3349b465..aca70ab2 100644 --- a/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs +++ b/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs @@ -1,7 +1,6 @@ using BrickController2.BusinessLogic; using BrickController2.CreationManagement; using BrickController2.CreationManagement.Sharing; -using BrickController2.DeviceManagement; using BrickController2.Helpers; using BrickController2.PlatformServices.SharedFileStorage; using BrickController2.UI.Commands; @@ -9,6 +8,8 @@ using BrickController2.UI.Services.Navigation; using BrickController2.UI.Services.Translation; using System; +using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using System.Windows.Input; @@ -21,7 +22,6 @@ public class CreationPageViewModel : PageViewModelBase private readonly IDialogService _dialogService; private readonly IPlayLogic _playLogic; private readonly ISharingManager _sharingManagerProfile; - private readonly IDeviceManager _deviceManager; public CreationPageViewModel( INavigationService navigationService, @@ -31,8 +31,7 @@ public CreationPageViewModel( ISharedFileStorageService sharedFileStorageService, IPlayLogic playLogic, ISharingManager sharingManagerProfile, - ICommandFactory commandFactory, - IDeviceManager deviceManager, + ICreationCommandFactory commandFactory, NavigationParameters parameters) : base(navigationService, translationService) { @@ -41,7 +40,6 @@ public CreationPageViewModel( SharedFileStorageService = sharedFileStorageService; _playLogic = playLogic; _sharingManagerProfile = sharingManagerProfile; - _deviceManager = deviceManager; Creation = parameters.Get("creation"); ImportControllerProfileCommand = new SafeCommand(async () => await ImportControllerProfileAsync(), () => SharedFileStorageService.IsSharedStorageAvailable); @@ -52,19 +50,19 @@ public CreationPageViewModel( RenameCreationCommand = new SafeCommand(async () => await RenameCreationAsync()); ShareCreationCommand = new SafeCommand(ShareCreationAsync); ShareCreationAsFileCommand = commandFactory.ShareAsJsonFileCommand(this, Creation); - PlayCommand = new SafeCommand(async () => await PlayAsync()); - FixItCommand = new SafeCommand(async () => await FixItAsync()); + PlayCommand = commandFactory.PlayCommand(this, Creation); + FixItCommand = commandFactory.FixCommand(this, Creation); AddControllerProfileCommand = new SafeCommand(async () => await AddControllerProfileAsync()); ControllerProfileTappedCommand = new SafeCommand(async controllerProfile => await NavigationService.NavigateToAsync(new NavigationParameters(("controllerprofile", controllerProfile)))); DeleteControllerProfileCommand = new SafeCommand(async controllerProfile => await DeleteControllerProfileAsync(controllerProfile)); - PlayControllerProfileCommand = new SafeCommand(PlayAsync); + PlayControllerProfileCommand = commandFactory.PlayControllerProfileCommand(this); } public Creation Creation { get; } public bool HasMultipleControllerProfiles => Creation.ControllerProfiles.Count > 1; - public bool IsCreationValid => _playLogic.ValidateCreation(Creation) == CreationValidationResult.Ok; + public bool IsCreationValid => Creation.ValidationResult == CreationValidationResult.Ok; public ISharedFileStorageService SharedFileStorageService { get; } public ICommand ImportControllerProfileCommand { get; } @@ -85,21 +83,39 @@ public CreationPageViewModel( public override void OnAppearing() { base.OnAppearing(); + + // listen to creation changes + Creation.PropertyChanged += Creation_PropertyChanged; + Creation.ControllerProfiles.CollectionChanged += ControllerProfiles_CollectionChanged; // recheck creation validity RecheckCreationValidity(); } - private void OnProfilesCountChanged() + public override void OnDisappearing() { - RaisePropertyChanged(nameof(HasMultipleControllerProfiles)); + Creation.PropertyChanged -= Creation_PropertyChanged; + Creation.ControllerProfiles.CollectionChanged -= ControllerProfiles_CollectionChanged; + + base.OnDisappearing(); } - private void RecheckCreationValidity() + private void Creation_PropertyChanged(object? sender, PropertyChangedEventArgs e) { - // recheck validity of the creation - RaisePropertyChanged(nameof(IsCreationValid)); + // notify update of creation validity + if (e.PropertyName == nameof(Creation.ValidationResult)) + { + RaisePropertyChanged(nameof(IsCreationValid)); + } } + private void ControllerProfiles_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + RaisePropertyChanged(nameof(HasMultipleControllerProfiles)); + RecheckCreationValidity(); + } + + private void RecheckCreationValidity() => Creation.ValidationResult = _playLogic.ValidateCreation(Creation); + private async Task RenameCreationAsync() { try @@ -136,188 +152,6 @@ await _dialogService.ShowProgressDialogAsync( } } - private async Task PlayAsync(ControllerProfile? controllerProfile = default!) - { - try - { - var validationResult = _playLogic.ValidateCreation(Creation); - - string warning = string.Empty; - switch (validationResult) - { - case CreationValidationResult.MissingControllerAction: - warning = Translate("NoControllerActions"); - break; - - case CreationValidationResult.MissingDevice: - warning = Translate("MissingDevices"); - break; - - case CreationValidationResult.MissingSequence: - warning = Translate("MissingSequence"); - break; - } - - if (validationResult == CreationValidationResult.Ok) - { - await NavigationService.NavigateToAsync(new NavigationParameters( - ("creation", Creation), - ("profile", controllerProfile!))); - } - else - { - await _dialogService.ShowMessageBoxAsync( - Translate("Warning"), - warning, - Translate("Ok"), - DisappearingToken); - } - } - catch (OperationCanceledException) - { - } - } - - private readonly struct DeviceModel(Device device) - { - public string DeviceId => device.Id; - public override string ToString() - { - return string.IsNullOrWhiteSpace(device.Name) ? device.Address : device.Name; - } - } - - private async Task FixItAsync() - { - try - { - var validationResult = _playLogic.ValidateCreation(Creation); - - if (validationResult == CreationValidationResult.MissingDevice) - { - // get missing device IDs - var deviceIds = _playLogic.GetMissingDevices(Creation) - .Select(id => - { - DeviceId.TryParse(id, out var deviceType, out var deviceAddress); - return (DeviceType: deviceType, Address: deviceAddress); - }) - .Where(x => x.DeviceType != DeviceType.Unknown && x.Address != null) - .ToList(); - - // get missing types - var missingTypes = deviceIds.Select(x => x.DeviceType).ToHashSet(); - // missing types that have some existing device of such type present - var suitableTypes = missingTypes.Where(x => _deviceManager.Devices.Any(d => d.DeviceType == x)) - .ToHashSet(); - - if (missingTypes.Count == 0 || suitableTypes.Count == 0) - { - // report error - no suitable missing devices - await _dialogService.ShowMessageBoxAsync( - Translate("Information"), - Translate("No suitable device found to replace missing device."), - Translate("Ok"), - DisappearingToken); - return; - } - - var sourceType = await ChooseDeviceTypeToRemapAsync(suitableTypes); - if (sourceType is null) - { - // user cancelled - return; - } - - // have device type - var missingDeviceAddresses = deviceIds - .Where(x => x.DeviceType == sourceType.Value) - .Select(x => x.Address!) - .ToList(); - - var sourceDeviceAddress = await ChooseDeviceAddressToRemapAsync(missingDeviceAddresses); - if (sourceDeviceAddress is null) - { - // user cancelled - return; - } - - // choose target device by name - var suitableDevices = _deviceManager.Devices - .Where(d => d.DeviceType == sourceType.Value) - .Select(d => new DeviceModel(d)) - .ToList(); - - var targetDevice = await _dialogService.ShowSelectionDialogAsync( - suitableDevices, - Translate("Target device"), - Translate("Cancel"), - DisappearingToken); - - if (targetDevice.IsOk) - { - // replace all missing device IDs with the selected one - var missingDeviceId = DeviceId.Get(sourceType.Value, sourceDeviceAddress); - var newDeviceId = targetDevice.SelectedItem!.DeviceId; - var count = await _creationManager.RemapDevice(Creation, missingDeviceId, newDeviceId); - - await _dialogService.ShowMessageBoxAsync( - Translate("Information"), - Translate($"Remapped {count} controller actions from device '{missingDeviceId}' to '{newDeviceId}'"), - Translate("Ok"), - DisappearingToken); - - RecheckCreationValidity(); - } - } - } - catch (OperationCanceledException) - { - } - } - - private async Task ChooseDeviceAddressToRemapAsync(System.Collections.Generic.List missingDeviceAddresses) - { - if (missingDeviceAddresses.Count == 1) - { - return missingDeviceAddresses[0]; - } - - // choose address to remap - var sourceDevice = await _dialogService.ShowSelectionDialogAsync( - missingDeviceAddresses, - Translate("Missing device"), - Translate("Cancel"), - DisappearingToken); - - if (sourceDevice.IsOk) - { - return sourceDevice.SelectedItem; - } - return default; - } - - private async Task ChooseDeviceTypeToRemapAsync(System.Collections.Generic.HashSet suitableTypes) - { - if (suitableTypes.Count == 1) - { - return suitableTypes.First(); - } - - // choose device type to remap - var deviceType = await _dialogService.ShowSelectionDialogAsync( - suitableTypes, - Translate("Missing device type"), - Translate("Cancel"), - DisappearingToken); - - if (deviceType.IsOk) - { - return deviceType.SelectedItem; - } - return default; - } - private async Task AddControllerProfileAsync() { try @@ -350,9 +184,6 @@ await _dialogService.ShowProgressDialogAsync( async (progressDialog, token) => controllerProfile = await _creationManager.AddControllerProfileAsync(Creation, result.Result), Translate("Creating"), token: DisappearingToken); - // notify profile count change - OnProfilesCountChanged(); - RecheckCreationValidity(); await NavigationService.NavigateToAsync(new NavigationParameters(("controllerprofile", controllerProfile!))); } @@ -378,9 +209,6 @@ await _dialogService.ShowProgressDialogAsync( async (progressDialog, token) => await _creationManager.DeleteControllerProfileAsync(controllerProfile), Translate("Deleting"), token: DisappearingToken); - // notify profile count change - OnProfilesCountChanged(); - RecheckCreationValidity(); } } catch (OperationCanceledException) @@ -406,9 +234,6 @@ private async Task ImportControllerProfileAsync() try { await _creationManager.ImportControllerProfileAsync(Creation, controllerProfileFilesMap[result.SelectedItem]); - // notify profile count change - OnProfilesCountChanged(); - RecheckCreationValidity(); } catch (Exception) { @@ -439,9 +264,6 @@ private async Task PasteControllerProfileAsync() { var profile = await _sharingManagerProfile.ImportFromClipboardAsync(); await _creationManager.ImportControllerProfileAsync(Creation, profile); - // notify profile count change - OnProfilesCountChanged(); - RecheckCreationValidity(); } catch (Exception ex) { From c37d8258dc43c212583990bff2f0b26ced91d474 Mon Sep 17 00:00:00 2001 From: Vit Nemecky Date: Tue, 2 Dec 2025 23:00:37 +0100 Subject: [PATCH 5/6] maker remapping generic - part I --- .../BusinessLogic/PlayLogic.cs | 2 +- .../CreationManagement/Creation.cs | 10 ++--- .../UI/Commands/CreationCommandFactory.cs | 44 ++++++++++--------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/BrickController2/BrickController2/BusinessLogic/PlayLogic.cs b/BrickController2/BrickController2/BusinessLogic/PlayLogic.cs index 319ec1f0..2b3ffc34 100644 --- a/BrickController2/BrickController2/BusinessLogic/PlayLogic.cs +++ b/BrickController2/BrickController2/BusinessLogic/PlayLogic.cs @@ -35,7 +35,7 @@ public CreationValidationResult ValidateCreation(Creation creation) var deviceIds = creation.GetDeviceIds(); var sequenceNames = creation.GetSequenceNames(); - if (deviceIds == null || deviceIds.Count() == 0) + if (deviceIds.Count == 0) { return CreationValidationResult.MissingControllerAction; } diff --git a/BrickController2/BrickController2/CreationManagement/Creation.cs b/BrickController2/BrickController2/CreationManagement/Creation.cs index 6c25bc69..7fcde1ca 100644 --- a/BrickController2/BrickController2/CreationManagement/Creation.cs +++ b/BrickController2/BrickController2/CreationManagement/Creation.cs @@ -57,9 +57,9 @@ public override string ToString() return Name; } - public IEnumerable GetDeviceIds() + public IReadOnlySet GetDeviceIds() { - var deviceIds = new List(); + var deviceIds = new HashSet(); foreach (var profile in ControllerProfiles) { @@ -67,11 +67,7 @@ public IEnumerable GetDeviceIds() { foreach (var controllerAction in controllerEvent.ControllerActions) { - var deviceId = controllerAction.DeviceId; - if (!deviceIds.Contains(deviceId)) - { - deviceIds.Add(deviceId); - } + deviceIds.Add(controllerAction.DeviceId); } } } diff --git a/BrickController2/BrickController2/UI/Commands/CreationCommandFactory.cs b/BrickController2/BrickController2/UI/Commands/CreationCommandFactory.cs index 59e120f5..4b620fd2 100644 --- a/BrickController2/BrickController2/UI/Commands/CreationCommandFactory.cs +++ b/BrickController2/BrickController2/UI/Commands/CreationCommandFactory.cs @@ -138,8 +138,9 @@ await DialogService.ShowMessageBoxAsync( private async Task FixMissingDevicesAsync(PageViewModelBase viewModel, Creation creation) { - // get missing device IDs - var deviceIds = _playLogic.GetMissingDevices(creation) + // get source device IDs + var sourceDeviceIds = //TODO _playLogic.GetMissingDevices(creation) + creation.GetDeviceIds() .Select(id => { DeviceId.TryParse(id, out var deviceType, out var deviceAddress); @@ -148,18 +149,20 @@ private async Task FixMissingDevicesAsync(PageViewModelBase viewModel, Cre .Where(x => x.DeviceType != DeviceType.Unknown && x.Address != null) .ToList(); - // get missing types - var missingTypes = deviceIds.Select(x => x.DeviceType).ToHashSet(); - // missing types that have some existing device of such type present - var suitableTypes = missingTypes.Where(x => _deviceManager.Devices.Any(d => d.DeviceType == x)) + // get source types + var sourceTypes = sourceDeviceIds.Select(x => x.DeviceType).ToHashSet(); + var addresses = sourceDeviceIds.Select(x => x.Address!).ToHashSet(); + // source types that have some existing device of such type present, but not used in creation + var suitableTypes = sourceTypes.Where(x => _deviceManager.Devices + .Any(d => d.DeviceType == x && !addresses.Contains(d.Address))) .ToHashSet(); - if (missingTypes.Count == 0 || suitableTypes.Count == 0) + if (sourceTypes.Count == 0 || suitableTypes.Count == 0) { - // report error - no suitable missing devices + // report error - no suitable device to replace await DialogService.ShowMessageBoxAsync( Translate("Information"), - Translate("No suitable device found to replace missing device."), + Translate("No suitable device found to remap a device."), Translate("Ok"), viewModel.DisappearingToken); return false; @@ -172,13 +175,13 @@ await DialogService.ShowMessageBoxAsync( return false; } - // have device type - var missingDeviceAddresses = deviceIds + // have source device type, get addresses of such type + var sourceDeviceAddresses = sourceDeviceIds .Where(x => x.DeviceType == sourceType.Value) .Select(x => x.Address!) .ToList(); - var sourceDeviceAddress = await ChooseDeviceAddressToRemapAsync(viewModel, missingDeviceAddresses); + var sourceDeviceAddress = await ChooseDeviceAddressToRemapAsync(viewModel, sourceDeviceAddresses); if (sourceDeviceAddress is null) { // user cancelled @@ -188,6 +191,7 @@ await DialogService.ShowMessageBoxAsync( // choose target device by name var suitableDevices = _deviceManager.Devices .Where(d => d.DeviceType == sourceType.Value) + .OrderBy(x => x.Name) .ToList(); var targetDevice = await DialogService.ShowSelectionDialogAsync( @@ -198,17 +202,17 @@ await DialogService.ShowMessageBoxAsync( if (targetDevice.IsOk) { - // replace all missing device IDs with the selected one - var missingDeviceId = DeviceId.Get(sourceType.Value, sourceDeviceAddress); + // replace all source device IDs with the selected one + var sourceDeviceId = DeviceId.Get(sourceType.Value, sourceDeviceAddress); var newDeviceId = targetDevice.SelectedItem!.Id; - var count = await _creationManager.RemapDevice(creation, missingDeviceId, newDeviceId); + var count = await _creationManager.RemapDevice(creation, sourceDeviceId, newDeviceId); // revalidate creation creation.ValidationResult = _playLogic.ValidateCreation(creation); await DialogService.ShowMessageBoxAsync( Translate("Information"), - Translate($"Remapped {count} controller actions from device '{missingDeviceId}' to '{newDeviceId}'"), + Translate($"Remapped {count} controller actions from device '{sourceDeviceId}' to '{newDeviceId}'"), Translate("Ok"), viewModel.DisappearingToken); } @@ -225,8 +229,8 @@ await DialogService.ShowMessageBoxAsync( // choose address to remap var sourceDevice = await DialogService.ShowSelectionDialogAsync( - missingDeviceAddresses, - Translate("Missing device"), + missingDeviceAddresses.Order(), + Translate("Source device"), Translate("Cancel"), viewModel.DisappearingToken); @@ -242,8 +246,8 @@ await DialogService.ShowMessageBoxAsync( // choose device type to remap var deviceType = await DialogService.ShowSelectionDialogAsync( - suitableTypes, - Translate("Missing device type"), + suitableTypes.OrderBy(x => x.ToString()), + Translate("Source device type"), Translate("Cancel"), viewModel.DisappearingToken); From 10f12a025521af3899fd4932e0fbed66936befb0 Mon Sep 17 00:00:00 2001 From: Vit Nemecky Date: Mon, 15 Dec 2025 19:18:06 +0100 Subject: [PATCH 6/6] simplify remapping scenario --- .../CreationManagement/Creation.cs | 21 +--------- .../UI/Commands/CreationCommandFactory.cs | 41 ++++-------------- .../UI/Commands/ICreationCommandFactory.cs | 2 +- .../UI/Pages/CreationPage.xaml | 10 +---- .../ViewModels/CreationListPageViewModel.cs | 5 --- .../UI/ViewModels/CreationPageViewModel.cs | 42 +------------------ 6 files changed, 14 insertions(+), 107 deletions(-) diff --git a/BrickController2/BrickController2/CreationManagement/Creation.cs b/BrickController2/BrickController2/CreationManagement/Creation.cs index 7fcde1ca..725caaba 100644 --- a/BrickController2/BrickController2/CreationManagement/Creation.cs +++ b/BrickController2/BrickController2/CreationManagement/Creation.cs @@ -1,5 +1,4 @@ -using BrickController2.BusinessLogic; -using BrickController2.CreationManagement.Sharing; +using BrickController2.CreationManagement.Sharing; using BrickController2.Helpers; using Newtonsoft.Json; using SQLite; @@ -13,7 +12,6 @@ public class Creation : NotifyPropertyChangedSource, IShareable { private string _name = string.Empty; private ObservableCollection _controllerProfiles = new ObservableCollection(); - private CreationValidationResult _lastValidation; [PrimaryKey, AutoIncrement] [JsonIgnore] @@ -32,23 +30,6 @@ public ObservableCollection ControllerProfiles set { _controllerProfiles = value; RaisePropertyChanged(); } } - /// - /// Keeps track of the last validation result for this creation. - /// - [JsonIgnore] - public CreationValidationResult ValidationResult - { - get { return _lastValidation; } - set - { - if (_lastValidation != value) - { - _lastValidation = value; - RaisePropertyChanged(); - } - } - } - [JsonIgnore] public static string Type => "bc2c"; diff --git a/BrickController2/BrickController2/UI/Commands/CreationCommandFactory.cs b/BrickController2/BrickController2/UI/Commands/CreationCommandFactory.cs index 4b620fd2..33f79a3c 100644 --- a/BrickController2/BrickController2/UI/Commands/CreationCommandFactory.cs +++ b/BrickController2/BrickController2/UI/Commands/CreationCommandFactory.cs @@ -47,8 +47,8 @@ public ICommand PlayCreationCommand(PageViewModelBase viewModel) public ICommand PlayControllerProfileCommand(PageViewModelBase viewModel) => new SafeCommand((profile) => PlayAsync(viewModel, profile.Creation, profile)); - public ICommand FixCommand(PageViewModelBase viewModel, Creation creation) - => new SafeCommand(() => FixItAsync(viewModel, creation)); + public ICommand RemapDeviceCommand(PageViewModelBase viewModel, Creation creation) + => new SafeCommand(() => RemapDeviceAsync(viewModel, creation)); protected override string ItemsTitle => Translate("Creations"); protected override string ItemNameHint => Translate("CreationName"); @@ -104,43 +104,21 @@ await DialogService.ShowMessageBoxAsync( } } - private async Task FixItAsync(PageViewModelBase viewModel, Creation creation) + private async Task RemapDeviceAsync(PageViewModelBase viewModel, Creation creation) { try { - var validationResult = _playLogic.ValidateCreation(creation); - - if (validationResult == CreationValidationResult.MissingDevice) - { - await FixMissingDevicesAsync(viewModel, creation); - } - else if (validationResult == CreationValidationResult.MissingSequence) - { - await DialogService.ShowMessageBoxAsync( - Translate("Warning"), - Translate("MissingSequence"), - Translate("Ok"), - viewModel.DisappearingToken); - } - else if (validationResult == CreationValidationResult.MissingControllerAction) - { - await DialogService.ShowMessageBoxAsync( - Translate("Warning"), - Translate("NoControllerActions"), - Translate("Ok"), - viewModel.DisappearingToken); - } + await RemapDevicesAsync(viewModel, creation); } catch (OperationCanceledException) { } } - private async Task FixMissingDevicesAsync(PageViewModelBase viewModel, Creation creation) + private async Task RemapDevicesAsync(PageViewModelBase viewModel, Creation creation) { // get source device IDs - var sourceDeviceIds = //TODO _playLogic.GetMissingDevices(creation) - creation.GetDeviceIds() + var sourceDeviceIds = creation.GetDeviceIds() .Select(id => { DeviceId.TryParse(id, out var deviceType, out var deviceAddress); @@ -188,9 +166,9 @@ await DialogService.ShowMessageBoxAsync( return false; } - // choose target device by name + // choose target device by name but avoid the source one var suitableDevices = _deviceManager.Devices - .Where(d => d.DeviceType == sourceType.Value) + .Where(d => d.DeviceType == sourceType.Value && d.Address != sourceDeviceAddress) .OrderBy(x => x.Name) .ToList(); @@ -207,9 +185,6 @@ await DialogService.ShowMessageBoxAsync( var newDeviceId = targetDevice.SelectedItem!.Id; var count = await _creationManager.RemapDevice(creation, sourceDeviceId, newDeviceId); - // revalidate creation - creation.ValidationResult = _playLogic.ValidateCreation(creation); - await DialogService.ShowMessageBoxAsync( Translate("Information"), Translate($"Remapped {count} controller actions from device '{sourceDeviceId}' to '{newDeviceId}'"), diff --git a/BrickController2/BrickController2/UI/Commands/ICreationCommandFactory.cs b/BrickController2/BrickController2/UI/Commands/ICreationCommandFactory.cs index 6d589216..76eafc01 100644 --- a/BrickController2/BrickController2/UI/Commands/ICreationCommandFactory.cs +++ b/BrickController2/BrickController2/UI/Commands/ICreationCommandFactory.cs @@ -12,5 +12,5 @@ public interface ICreationCommandFactory : ICommandFactory ICommand PlayControllerProfileCommand(PageViewModelBase viewModel); - ICommand FixCommand(PageViewModelBase viewModel, Creation creation); + ICommand RemapDeviceCommand(PageViewModelBase viewModel, Creation creation); } diff --git a/BrickController2/BrickController2/UI/Pages/CreationPage.xaml b/BrickController2/BrickController2/UI/Pages/CreationPage.xaml index be77723b..21c921cd 100644 --- a/BrickController2/BrickController2/UI/Pages/CreationPage.xaml +++ b/BrickController2/BrickController2/UI/Pages/CreationPage.xaml @@ -13,12 +13,6 @@ ios:Page.UseSafeArea="True" BackgroundColor="{DynamicResource PageBackgroundColor}"> - - - - - - @@ -26,6 +20,7 @@ + @@ -43,8 +38,7 @@ - - + diff --git a/BrickController2/BrickController2/UI/ViewModels/CreationListPageViewModel.cs b/BrickController2/BrickController2/UI/ViewModels/CreationListPageViewModel.cs index a35b2065..35c0fe41 100644 --- a/BrickController2/BrickController2/UI/ViewModels/CreationListPageViewModel.cs +++ b/BrickController2/BrickController2/UI/ViewModels/CreationListPageViewModel.cs @@ -98,11 +98,6 @@ public override async void OnAppearing() await LoadCreationsAndDevicesAsync(); await RequestPermissionsAsync(); } - // update validation statuses - foreach (var creation in _creationManager.Creations) - { - creation.ValidationResult = _playLogic.ValidateCreation(creation); - } } public override void OnDisappearing() diff --git a/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs b/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs index aca70ab2..a2a8cbb4 100644 --- a/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs +++ b/BrickController2/BrickController2/UI/ViewModels/CreationPageViewModel.cs @@ -51,7 +51,7 @@ public CreationPageViewModel( ShareCreationCommand = new SafeCommand(ShareCreationAsync); ShareCreationAsFileCommand = commandFactory.ShareAsJsonFileCommand(this, Creation); PlayCommand = commandFactory.PlayCommand(this, Creation); - FixItCommand = commandFactory.FixCommand(this, Creation); + RemapDeviceCommand = commandFactory.RemapDeviceCommand(this, Creation); AddControllerProfileCommand = new SafeCommand(async () => await AddControllerProfileAsync()); ControllerProfileTappedCommand = new SafeCommand(async controllerProfile => await NavigationService.NavigateToAsync(new NavigationParameters(("controllerprofile", controllerProfile)))); DeleteControllerProfileCommand = new SafeCommand(async controllerProfile => await DeleteControllerProfileAsync(controllerProfile)); @@ -62,8 +62,6 @@ public CreationPageViewModel( public bool HasMultipleControllerProfiles => Creation.ControllerProfiles.Count > 1; - public bool IsCreationValid => Creation.ValidationResult == CreationValidationResult.Ok; - public ISharedFileStorageService SharedFileStorageService { get; } public ICommand ImportControllerProfileCommand { get; } public ICommand CopyControllerProfileCommand { get; } @@ -74,48 +72,12 @@ public CreationPageViewModel( public ICommand ShareCreationAsFileCommand { get; } public ICommand RenameCreationCommand { get; } public ICommand PlayCommand { get; } - public ICommand FixItCommand { get; } + public ICommand RemapDeviceCommand { get; } public ICommand AddControllerProfileCommand { get; } public ICommand ControllerProfileTappedCommand { get; } public ICommand DeleteControllerProfileCommand { get; } public ICommand PlayControllerProfileCommand { get; } - public override void OnAppearing() - { - base.OnAppearing(); - - // listen to creation changes - Creation.PropertyChanged += Creation_PropertyChanged; - Creation.ControllerProfiles.CollectionChanged += ControllerProfiles_CollectionChanged; - // recheck creation validity - RecheckCreationValidity(); - } - - public override void OnDisappearing() - { - Creation.PropertyChanged -= Creation_PropertyChanged; - Creation.ControllerProfiles.CollectionChanged -= ControllerProfiles_CollectionChanged; - - base.OnDisappearing(); - } - - private void Creation_PropertyChanged(object? sender, PropertyChangedEventArgs e) - { - // notify update of creation validity - if (e.PropertyName == nameof(Creation.ValidationResult)) - { - RaisePropertyChanged(nameof(IsCreationValid)); - } - } - - private void ControllerProfiles_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - RaisePropertyChanged(nameof(HasMultipleControllerProfiles)); - RecheckCreationValidity(); - } - - private void RecheckCreationValidity() => Creation.ValidationResult = _playLogic.ValidateCreation(Creation); - private async Task RenameCreationAsync() { try