diff --git a/.idea/.idea.RoadCaptain.Windows/.idea/avalonia.xml b/.idea/.idea.RoadCaptain.Windows/.idea/avalonia.xml index 2b5b0b69..ee7f6d79 100644 --- a/.idea/.idea.RoadCaptain.Windows/.idea/avalonia.xml +++ b/.idea/.idea.RoadCaptain.Windows/.idea/avalonia.xml @@ -4,12 +4,15 @@ diff --git a/src/RoadCaptain.Adapters/HttpRouteRepository.cs b/src/RoadCaptain.Adapters/HttpRouteRepository.cs index 603a6ad2..2a9d220a 100644 --- a/src/RoadCaptain.Adapters/HttpRouteRepository.cs +++ b/src/RoadCaptain.Adapters/HttpRouteRepository.cs @@ -209,5 +209,10 @@ public async Task StoreAsync(PlannedRoute plannedRoute, string? toke return null; } } + + public Task DeleteAsync(Uri routeUri) + { + throw new NotImplementedException(); + } } } diff --git a/src/RoadCaptain.Adapters/LocalDirectoryRouteRepository.cs b/src/RoadCaptain.Adapters/LocalDirectoryRouteRepository.cs index 608540df..8682ad33 100644 --- a/src/RoadCaptain.Adapters/LocalDirectoryRouteRepository.cs +++ b/src/RoadCaptain.Adapters/LocalDirectoryRouteRepository.cs @@ -236,5 +236,10 @@ protected virtual Task WriteAllTextAsync(string path, string serialized) return null; } } + + public Task DeleteAsync(Uri routeUri) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/src/RoadCaptain.Adapters/RebelRouteRepository.cs b/src/RoadCaptain.Adapters/RebelRouteRepository.cs index 4da0d40d..b5409907 100644 --- a/src/RoadCaptain.Adapters/RebelRouteRepository.cs +++ b/src/RoadCaptain.Adapters/RebelRouteRepository.cs @@ -157,6 +157,10 @@ public Task StoreAsync(PlannedRoute plannedRoute, string? token, Lis public string Name => "Zwift Insider - Rebel Routes"; public bool IsReadOnly => true; + public Task DeleteAsync(Uri routeUri) + { + throw new InvalidOperationException("Rebel Routes are baked into RoadCaptain and can't be deleted."); + } private PlannedRoute? UpgradeIfNecessaryAndSerialize(string? routeModelSerialized) { diff --git a/src/RoadCaptain.App.RouteBuilder/DelegateDecorator.cs b/src/RoadCaptain.App.RouteBuilder/DelegateDecorator.cs index 3d67ef0d..e1a161ff 100644 --- a/src/RoadCaptain.App.RouteBuilder/DelegateDecorator.cs +++ b/src/RoadCaptain.App.RouteBuilder/DelegateDecorator.cs @@ -70,6 +70,11 @@ public async Task ShowWhatIsNewDialog(Release release) return await InvokeIfNeededAsync(() => _decorated.ShowOpenRouteDialog()); } + public async Task ShowQuestionDialog(string title, string message) + { + return await InvokeIfNeededAsync(() => _decorated.ShowQuestionDialog(title, message)); + } + public async Task ShowDefaultSportSelectionDialog(SportType sport) { return await InvokeIfNeededAsync(() => _decorated.ShowDefaultSportSelectionDialog(sport)); diff --git a/src/RoadCaptain.App.RouteBuilder/DesignTimeWindowService.cs b/src/RoadCaptain.App.RouteBuilder/DesignTimeWindowService.cs index a4102531..b1041a75 100644 --- a/src/RoadCaptain.App.RouteBuilder/DesignTimeWindowService.cs +++ b/src/RoadCaptain.App.RouteBuilder/DesignTimeWindowService.cs @@ -59,6 +59,11 @@ public Task ShowWhatIsNewDialog(Release release) throw new System.NotImplementedException(); } + public Task ShowQuestionDialog(string title, string message) + { + throw new System.NotImplementedException(); + } + public Task ShowDefaultSportSelectionDialog(SportType sport) { throw new System.NotImplementedException(); diff --git a/src/RoadCaptain.App.RouteBuilder/IWindowService.cs b/src/RoadCaptain.App.RouteBuilder/IWindowService.cs index 9cdccb89..4822d525 100644 --- a/src/RoadCaptain.App.RouteBuilder/IWindowService.cs +++ b/src/RoadCaptain.App.RouteBuilder/IWindowService.cs @@ -23,5 +23,6 @@ public interface IWindowService : RoadCaptain.App.Shared.IWindowService Window? GetCurrentWindow(); Task ShowSaveFileDialog(string? previousLocation, string? suggestedFileName = null); Task<(PlannedRoute? PlannedRoute, string? RouteFilePath)> ShowOpenRouteDialog(); + Task ShowQuestionDialog(string title, string message); } } \ No newline at end of file diff --git a/src/RoadCaptain.App.RouteBuilder/ViewModels/DesignTimeManageRoutesViewModel.cs b/src/RoadCaptain.App.RouteBuilder/ViewModels/DesignTimeManageRoutesViewModel.cs new file mode 100644 index 00000000..ce2c41b6 --- /dev/null +++ b/src/RoadCaptain.App.RouteBuilder/ViewModels/DesignTimeManageRoutesViewModel.cs @@ -0,0 +1,63 @@ +using System.Collections.Immutable; +using RoadCaptain.UseCases; + +namespace RoadCaptain.App.RouteBuilder.ViewModels +{ + public class DesignTimeManageRoutesViewModel : ManageRoutesViewModel + { + public DesignTimeManageRoutesViewModel() + : base(new RetrieveRepositoryNamesUseCase(new[] { new StubRouteRepository() }), null!, new DeleteRouteUseCase(new []{new StubRouteRepository()})) + { + Repositories = new[] + { + "All", + "Local" + }.ToImmutableList(); + + Routes = new[] + { + new RoadCaptain.App.Shared.ViewModels.RouteViewModel(new RouteModel + { + Ascent = 123, + Descent = 75, + Distance = 105, + CreatorName = "Joe Bloegs", + World = "watopia", + Id = 1, + IsLoop = false, + Name = "Design time route 1", + RepositoryName = "Local", + ZwiftRouteName = "ZRName1" + }), + + new RoadCaptain.App.Shared.ViewModels.RouteViewModel(new RouteModel + { + Ascent = 13, + Descent = 45, + Distance = 45, + CreatorName = "Joe Blogs", + World = "yorkshire", + Id = 2, + IsLoop = true, + Name = "Design time route 2", + RepositoryName = "Local", + ZwiftRouteName = "ZRName2" + }), + + new RoadCaptain.App.Shared.ViewModels.RouteViewModel(new RouteModel + { + Ascent = 13, + Descent = 45, + Distance = 45, + CreatorName = "Joe Blogs", + World = "makuri_islands", + Id = 3, + IsLoop = true, + Name = "Design time route 3", + RepositoryName = "Local", + ZwiftRouteName = "ZRName3" + }) + }.ToImmutableList(); + } + } +} \ No newline at end of file diff --git a/src/RoadCaptain.App.RouteBuilder/ViewModels/DesignTimeSaveRouteDialogViewModel.cs b/src/RoadCaptain.App.RouteBuilder/ViewModels/DesignTimeSaveRouteDialogViewModel.cs index dcc6bc96..2b6914f2 100644 --- a/src/RoadCaptain.App.RouteBuilder/ViewModels/DesignTimeSaveRouteDialogViewModel.cs +++ b/src/RoadCaptain.App.RouteBuilder/ViewModels/DesignTimeSaveRouteDialogViewModel.cs @@ -2,6 +2,7 @@ // Licensed under Artistic License 2.0 // See LICENSE or https://choosealicense.com/licenses/artistic-2.0/ +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Threading.Tasks; @@ -64,5 +65,9 @@ public Task StoreAsync(PlannedRoute plannedRoute, string? token, Lis public string Name => "Local"; public bool IsReadOnly => false; + public Task DeleteAsync(Uri routeUri) + { + throw new NotImplementedException(); + } } } diff --git a/src/RoadCaptain.App.RouteBuilder/ViewModels/ManageRoutesViewModel.cs b/src/RoadCaptain.App.RouteBuilder/ViewModels/ManageRoutesViewModel.cs new file mode 100644 index 00000000..389cfa52 --- /dev/null +++ b/src/RoadCaptain.App.RouteBuilder/ViewModels/ManageRoutesViewModel.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using ReactiveUI; +using RoadCaptain.App.Shared.Commands; +using RoadCaptain.App.Shared.Dialogs; +using RoadCaptain.Commands; +using RoadCaptain.UseCases; + +namespace RoadCaptain.App.RouteBuilder.ViewModels +{ + public class ManageRoutesViewModel : ViewModelBase + { + private ImmutableList? _repositories; + private string? _selectedRepository; + private readonly RetrieveRepositoryNamesUseCase _retrieveRepositoryNamesUseCase; + private ImmutableList? _routes; + private readonly IWindowService _windowService; + private readonly DeleteRouteUseCase _deleteRouteUseCase; + + public ManageRoutesViewModel(RetrieveRepositoryNamesUseCase retrieveRepositoryNamesUseCase, IWindowService windowService, DeleteRouteUseCase deleteRouteUseCase) + { + _retrieveRepositoryNamesUseCase = retrieveRepositoryNamesUseCase; + _windowService = windowService; + _deleteRouteUseCase = deleteRouteUseCase; + } + + public AsyncRelayCommand DeleteRouteCommand => new AsyncRelayCommand( + parameter => DeleteRouteAsync((parameter as Shared.ViewModels.RouteViewModel)!), + parameter => parameter is Shared.ViewModels.RouteViewModel); + + private async Task DeleteRouteAsync(Shared.ViewModels.RouteViewModel parameter) + { + var result = await _windowService.ShowQuestionDialog( + "Delete route?", + $"Are you sure you want to delete the route {parameter.Name}?"); + + if (result == MessageBoxResult.No) + { + return CommandResult.Aborted(); + } + + try + { + await _deleteRouteUseCase.ExecuteAsync(new DeleteRouteCommand(parameter.Uri, parameter.RepositoryName)); + } + catch (Exception e) + { + return CommandResult.Failure($"Failed to delete route: {e.Message}"); + } + + return CommandResult.Success(); + } + + public Task InitializeAsync() + { + Repositories = _retrieveRepositoryNamesUseCase.Execute(new RetrieveRepositoryNamesCommand(RetrieveRepositoriesIntent.Manage)).ToImmutableList(); + + return Task.CompletedTask; + } + + public ImmutableList Repositories + { + get => _repositories ?? ImmutableList.Empty; + set + { + if (value == _repositories) + { + return; + } + + _repositories = value; + this.RaisePropertyChanged(); + } + } + + public string? SelectedRepository + { + get => _selectedRepository; + set + { + if (value == _selectedRepository) + { + return; + } + + _selectedRepository = value; + this.RaisePropertyChanged(); + } + } + + public ImmutableList Routes + { + get => _routes ?? ImmutableList.Empty; + set + { + if (_routes != null && value == _routes) + { + return; + } + + _routes = value; + + this.RaisePropertyChanged(); + } + } + } +} \ No newline at end of file diff --git a/src/RoadCaptain.App.RouteBuilder/Views/ManageRoutes.axaml b/src/RoadCaptain.App.RouteBuilder/Views/ManageRoutes.axaml new file mode 100644 index 00000000..4b1360c5 --- /dev/null +++ b/src/RoadCaptain.App.RouteBuilder/Views/ManageRoutes.axaml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +