diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Extensions/DisposableExtensions.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Extensions/DisposableExtensions.cs new file mode 100644 index 0000000..86e89c0 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Extensions/DisposableExtensions.cs @@ -0,0 +1,9 @@ +using System.Collections.ObjectModel; + +namespace FileTime.App.Core.Extensions; + +public static class DisposableExtensions +{ + public static void AddToDisposables(this IDisposable disposable, ICollection collection) + => collection.Add(disposable); +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs index 3e0166c..c794826 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs @@ -7,7 +7,7 @@ using InitableService; namespace FileTime.App.Core.ViewModels; -public interface ITabViewModel : IInitable +public interface ITabViewModel : IInitable, IDisposable { ITab? Tab { get; } int TabNumber { get; } diff --git a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs index 4acbdd5..107c059 100644 --- a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs @@ -22,6 +22,7 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi private readonly IInputInterface _inputInterface; private readonly ILogger _logger; private readonly ITimelessContentProvider _timelessContentProvider; + private readonly ICommandScheduler _commandScheduler; private readonly BindedCollection? _markedItems; private PointInTime _currentPointInTime; @@ -31,13 +32,15 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi IClipboardService clipboardService, IInputInterface inputInterface, ILogger logger, - ITimelessContentProvider timelessContentProvider) : base(appState, timelessContentProvider) + ITimelessContentProvider timelessContentProvider, + ICommandScheduler commandScheduler) : base(appState, timelessContentProvider) { _userCommandHandlerService = userCommandHandlerService; _clipboardService = clipboardService; _inputInterface = inputInterface; _logger = logger; _timelessContentProvider = timelessContentProvider; + _commandScheduler = commandScheduler; _currentPointInTime = null!; SaveSelectedTab(t => _selectedTab = t); @@ -128,5 +131,6 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi var newContainerName = containerNameInput.Value; var command = new CreateContainerCommand(); + await _commandScheduler.AddCommand(command); } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs index bb7a7f2..e2da064 100644 --- a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs @@ -67,7 +67,8 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase private Task OpenSelected() { - if (_currentSelectedItem is not IContainerViewModel containerViewModel || containerViewModel.Container is null) return Task.CompletedTask; + if (_currentSelectedItem is not IContainerViewModel containerViewModel || containerViewModel.Container is null) + return Task.CompletedTask; _selectedTab?.Tab?.SetCurrentLocation(containerViewModel.Container); return Task.CompletedTask; @@ -75,7 +76,8 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase private async Task GoUp() { - if (_currentLocation?.Parent is not AbsolutePath parentPath || await parentPath.ResolveAsyncSafe() is not IContainer newContainer) return; + if (_currentLocation?.Parent is not AbsolutePath parentPath || + await parentPath.ResolveAsyncSafe() is not IContainer newContainer) return; _selectedTab?.Tab?.SetCurrentLocation(newContainer); } @@ -125,7 +127,8 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase } else if (tabViewModel == null) { - var tab = _serviceProvider.GetInitableResolver(_currentLocation ?? _localContentProvider).GetRequiredService(); + var tab = _serviceProvider.GetInitableResolver(_currentLocation ?? _localContentProvider) + .GetRequiredService(); var newTabViewModel = _serviceProvider.GetInitableResolver(tab, number).GetRequiredService(); _appState.AddTab(newTabViewModel); @@ -144,9 +147,18 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase private Task CloseTab() { - if (_appState.Tabs.Count < 2) return Task.CompletedTask; + if (_appState.Tabs.Count < 2 || _selectedTab == null) return Task.CompletedTask; - _appState.RemoveTab(_selectedTab!); + var tabToRemove = _selectedTab; + _appState.RemoveTab(tabToRemove!); + + try + { + tabToRemove.Dispose(); + } + catch + { + } return Task.CompletedTask; } diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs index fd5c1d1..7f02b0b 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs @@ -13,7 +13,7 @@ using MvvmGen; namespace FileTime.App.Core.ViewModels; [ViewModel] -public partial class TabViewModel : ITabViewModel, IDisposable +public partial class TabViewModel : ITabViewModel { private readonly IServiceProvider _serviceProvider; private readonly IItemNameConverterService _itemNameConverterService; @@ -70,6 +70,8 @@ public partial class TabViewModel : ITabViewModel, IDisposable Tab = tab; TabNumber = tabNumber; + tab.AddToDisposables(_disposables); + CurrentLocation = tab.CurrentLocation.AsObservable(); CurrentItems = tab.CurrentItems .Select(items => items?.Transform(i => MapItemToViewModel(i, ItemViewModelType.Main))) @@ -105,7 +107,7 @@ public partial class TabViewModel : ITabViewModel, IDisposable ParentsChildrenCollection = new(ParentsChildren); SelectedsChildrenCollection = new(SelectedsChildren); - tab.CurrentLocation.Subscribe((_) => _markedItems.Clear()); + tab.CurrentLocation.Subscribe((_) => _markedItems.Clear()).AddToDisposables(_disposables); IObservable>?> InitSelectedsChildren() { diff --git a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs index 3caac05..fa518ff 100644 --- a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs +++ b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs @@ -1,8 +1,10 @@ using FileTime.App.Core; +using FileTime.Core.Command; using FileTime.Core.Services; using FileTime.Core.Timeline; using FileTime.Providers.Local; using Microsoft.Extensions.DependencyInjection; +using ICommandExecutor = FileTime.Core.Timeline.ICommandExecutor; namespace FileTime.App.DependencyInjection; @@ -13,9 +15,11 @@ public static class DependencyInjection serviceCollection ??= new ServiceCollection(); return serviceCollection - .AddTransient() .AddSingleton() .AddSingleton() + .AddSingleton() + .AddTransient() + .AddTransient() .AddCoreAppServices() .AddLocalServices(); } diff --git a/src/Core/FileTime.Core.Abstraction/Command/ExecutionState.cs b/src/Core/FileTime.Core.Abstraction/Command/ExecutionState.cs new file mode 100644 index 0000000..584be10 --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/Command/ExecutionState.cs @@ -0,0 +1,8 @@ +namespace FileTime.Core.Command; + +public enum ExecutionState +{ + Waiting, + Running, + Finished +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Command/ICommand.cs b/src/Core/FileTime.Core.Abstraction/Command/ICommand.cs index 9e41fa5..6b494f2 100644 --- a/src/Core/FileTime.Core.Abstraction/Command/ICommand.cs +++ b/src/Core/FileTime.Core.Abstraction/Command/ICommand.cs @@ -5,5 +5,5 @@ namespace FileTime.Core.Command; public interface ICommand { Task CanRun(PointInTime currentTime); - Task SimulateCommand(PointInTime? currentTime); + Task SimulateCommand(PointInTime currentTime); } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Command/ICommandHandler.cs b/src/Core/FileTime.Core.Abstraction/Command/ICommandHandler.cs new file mode 100644 index 0000000..94b262b --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/Command/ICommandHandler.cs @@ -0,0 +1,9 @@ +using FileTime.Core.Timeline; + +namespace FileTime.Core.Command; + +public interface ICommandHandler +{ + bool CanHandle(ICommand command); + Task ExecuteAsync(ICommand command); +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Command/ICommandRunner.cs b/src/Core/FileTime.Core.Abstraction/Command/ICommandRunner.cs new file mode 100644 index 0000000..cc8ee0b --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/Command/ICommandRunner.cs @@ -0,0 +1,8 @@ +using FileTime.Core.Timeline; + +namespace FileTime.Core.Command; + +public interface ICommandRunner +{ + Task RunCommandAsync(ICommand command); +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Command/IExecutableCommand.cs b/src/Core/FileTime.Core.Abstraction/Command/IExecutableCommand.cs index 786bfad..3f94c04 100644 --- a/src/Core/FileTime.Core.Abstraction/Command/IExecutableCommand.cs +++ b/src/Core/FileTime.Core.Abstraction/Command/IExecutableCommand.cs @@ -4,5 +4,5 @@ namespace FileTime.Core.Command; public interface IExecutableCommand : ICommand { - Task Execute(ICommandScheduler commandScheduler); + Task Execute(); } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Extensions/DynamicDataExtensions.cs b/src/Core/FileTime.Core.Abstraction/Extensions/DynamicDataExtensions.cs new file mode 100644 index 0000000..beed8ce --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/Extensions/DynamicDataExtensions.cs @@ -0,0 +1,73 @@ +using System.Reactive.Linq; +using DynamicData; +using FileTime.Core.Models; + +namespace FileTime.Core.Extensions; + +public static class DynamicDataExtensions +{ + private class DisposableContext + { + private readonly Func _transformResult; + private readonly TaskCompletionSource _taskCompletionSource; + public IDisposable? Disposable { get; set; } + + public DisposableContext(Func transformResult, + TaskCompletionSource taskCompletionSource, IDisposable? disposable = null) + { + _transformResult = transformResult; + _taskCompletionSource = taskCompletionSource; + Disposable = disposable; + } + + public void OnNext(TParam param) + { + Disposable?.Dispose(); + var result = _transformResult(param); + _taskCompletionSource.SetResult(result); + } + + public void OnError(Exception ex) + { + Disposable?.Dispose(); + _taskCompletionSource.SetException(ex); + } + + public void OnCompleted() + { + Disposable?.Dispose(); + _taskCompletionSource.SetResult(default(TTaskResult)); + } + } + + public static async Task?> GetItemsAsync( + this IObservable>?> stream) + => await GetItemsAsync(stream + .Select(s => + s is null + ? new SourceList().Connect().StartWithEmpty().ToCollection() + : s.ToCollection()) + .Switch()); + + public static async Task?> GetItemsAsync( + this IObservable> stream) + => await GetItemsAsync(stream.ToCollection()); + + private static Task?> GetItemsAsync( + this IObservable> stream) + { + var taskCompletionSource = new TaskCompletionSource?>(); + var context = new DisposableContext, IEnumerable?>(r => r, + taskCompletionSource); + + var disposable = stream + .Subscribe( + context.OnNext, + context.OnError, + context.OnCompleted + ); + context.Disposable = disposable; + + return taskCompletionSource.Task; + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/FileTime.Core.Abstraction.csproj b/src/Core/FileTime.Core.Abstraction/FileTime.Core.Abstraction.csproj index 473be95..caad2a1 100644 --- a/src/Core/FileTime.Core.Abstraction/FileTime.Core.Abstraction.csproj +++ b/src/Core/FileTime.Core.Abstraction/FileTime.Core.Abstraction.csproj @@ -24,4 +24,8 @@ + + + + diff --git a/src/Core/FileTime.Core.Abstraction/Models/FullName.cs b/src/Core/FileTime.Core.Abstraction/Models/FullName.cs index d57a913..a1fdc9e 100644 --- a/src/Core/FileTime.Core.Abstraction/Models/FullName.cs +++ b/src/Core/FileTime.Core.Abstraction/Models/FullName.cs @@ -4,8 +4,6 @@ public record FullName(string Path) { public FullName? GetParent() { - if (Path is null) return null; - var pathParts = Path.TrimEnd(Constants.SeparatorChar).Split(Constants.SeparatorChar); return pathParts.Length switch { @@ -13,4 +11,10 @@ public record FullName(string Path) _ => null }; } + + public string GetName() + => Path.Split(Constants.SeparatorChar).Last(); + + public FullName GetChild(string childName) + => new FullName(Path + Constants.SeparatorChar + childName); } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Services/ITab.cs b/src/Core/FileTime.Core.Abstraction/Services/ITab.cs index ded0da7..b163ec2 100644 --- a/src/Core/FileTime.Core.Abstraction/Services/ITab.cs +++ b/src/Core/FileTime.Core.Abstraction/Services/ITab.cs @@ -4,7 +4,7 @@ using InitableService; namespace FileTime.Core.Services; -public interface ITab : IInitable +public interface ITab : IInitable, IDisposable { IObservable CurrentLocation { get; } IObservable CurrentSelectedItem { get; } diff --git a/src/Core/FileTime.Core.Abstraction/Timeline/CommandTimeState.cs b/src/Core/FileTime.Core.Abstraction/Timeline/CommandTimeState.cs index 8b0b2e7..da7ca5a 100644 --- a/src/Core/FileTime.Core.Abstraction/Timeline/CommandTimeState.cs +++ b/src/Core/FileTime.Core.Abstraction/Timeline/CommandTimeState.cs @@ -7,6 +7,7 @@ public class CommandTimeState public ICommand Command { get; } public CanCommandRun CanRun { get; private set; } = CanCommandRun.False; public bool ForceRun { get; set; } + public ExecutionState ExecutionState { get; set; } public CommandTimeState(ICommand command, PointInTime? startTime) { diff --git a/src/Core/FileTime.Core.Abstraction/Timeline/ICommandExecutor.cs b/src/Core/FileTime.Core.Abstraction/Timeline/ICommandExecutor.cs new file mode 100644 index 0000000..c0eaf69 --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/Timeline/ICommandExecutor.cs @@ -0,0 +1,9 @@ +using FileTime.Core.Command; + +namespace FileTime.Core.Timeline; + +public interface ICommandExecutor +{ + void ExecuteCommand(ICommand command); + event EventHandler CommandFinished; +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Timeline/ICommandScheduler.cs b/src/Core/FileTime.Core.Abstraction/Timeline/ICommandScheduler.cs index 0046851..c62703a 100644 --- a/src/Core/FileTime.Core.Abstraction/Timeline/ICommandScheduler.cs +++ b/src/Core/FileTime.Core.Abstraction/Timeline/ICommandScheduler.cs @@ -1,6 +1,8 @@ +using FileTime.Core.Command; + namespace FileTime.Core.Timeline; public interface ICommandScheduler { - + Task AddCommand(ICommand command, int? batchId = null, bool toNewBatch = false); } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Timeline/ILocalCommandExecutor.cs b/src/Core/FileTime.Core.Abstraction/Timeline/ILocalCommandExecutor.cs new file mode 100644 index 0000000..28022ad --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/Timeline/ILocalCommandExecutor.cs @@ -0,0 +1,6 @@ +namespace FileTime.Core.Timeline; + +public interface ILocalCommandExecutor : ICommandExecutor +{ + +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Timeline/ParallelCommands.cs b/src/Core/FileTime.Core.Abstraction/Timeline/ParallelCommands.cs index 0b84227..bdee8ad 100644 --- a/src/Core/FileTime.Core.Abstraction/Timeline/ParallelCommands.cs +++ b/src/Core/FileTime.Core.Abstraction/Timeline/ParallelCommands.cs @@ -5,7 +5,7 @@ namespace FileTime.Core.Timeline; public class ParallelCommands { private static ushort _idCounter; - public List _commands; + private List _commands; public ushort Id { get; } public IReadOnlyList Commands { get; } public PointInTime? Result { get; private set; } diff --git a/src/Core/FileTime.Core.Abstraction/Timeline/PointInTime.cs b/src/Core/FileTime.Core.Abstraction/Timeline/PointInTime.cs index 7b58d77..42a46ce 100644 --- a/src/Core/FileTime.Core.Abstraction/Timeline/PointInTime.cs +++ b/src/Core/FileTime.Core.Abstraction/Timeline/PointInTime.cs @@ -2,10 +2,11 @@ namespace FileTime.Core.Timeline; public class PointInTime { - private readonly List _differences; public static readonly PointInTime Eternal = new PointInTime(); public static readonly PointInTime Present = new PointInTime(); + private readonly List _differences; + public IReadOnlyList Differences { get; } private PointInTime() : this(new List()) @@ -26,6 +27,14 @@ public class PointInTime public PointInTime WithDifferences(IEnumerable differences) => new(this, differences); + public PointInTime WithDifferences(Func> differenceGenerator) + { + var newPointInTime = new PointInTime(); + newPointInTime._differences.AddRange(differenceGenerator(newPointInTime)); + + return newPointInTime; + } + private static List MergeDifferences(IEnumerable previouses, IEnumerable differences) { diff --git a/src/Core/FileTime.Core.Command/CommandRunner.cs b/src/Core/FileTime.Core.Command/CommandRunner.cs new file mode 100644 index 0000000..9fa310e --- /dev/null +++ b/src/Core/FileTime.Core.Command/CommandRunner.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.Logging; + +namespace FileTime.Core.Command; + +public class CommandRunner : ICommandRunner +{ + private readonly List _commandHandlers; + private readonly ILogger _logger; + + public CommandRunner( + IEnumerable commandHandlers, + ILogger logger) + { + _commandHandlers = commandHandlers.ToList(); + _logger = logger; + } + + public async Task RunCommandAsync(ICommand command) + { + if (command is IExecutableCommand executableCommand) + { + await executableCommand.Execute(); + } + else + { + var commandHandler = _commandHandlers.Find(c => c.CanHandle(command)); + if (commandHandler != null) + { + await commandHandler.ExecuteAsync(command); + } + else + { + _logger.LogError("No command handler for command {Command}", command.GetType().Name); + } + } + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Command/Copy/CopyCommand.cs b/src/Core/FileTime.Core.Command/Copy/CopyCommand.cs index e7d44fc..e838cd1 100644 --- a/src/Core/FileTime.Core.Command/Copy/CopyCommand.cs +++ b/src/Core/FileTime.Core.Command/Copy/CopyCommand.cs @@ -9,7 +9,7 @@ public class CopyCommand : ITransportationCommand throw new NotImplementedException(); } - public Task SimulateCommand(PointInTime? currentTime) + public Task SimulateCommand(PointInTime currentTime) { throw new NotImplementedException(); } diff --git a/src/Core/FileTime.Core.Command/CreateContainer/CreateContainerCommand.cs b/src/Core/FileTime.Core.Command/CreateContainer/CreateContainerCommand.cs index e5286ef..13b2b9b 100644 --- a/src/Core/FileTime.Core.Command/CreateContainer/CreateContainerCommand.cs +++ b/src/Core/FileTime.Core.Command/CreateContainer/CreateContainerCommand.cs @@ -1,21 +1,76 @@ +using FileTime.Core.Enums; +using FileTime.Core.Extensions; +using FileTime.Core.Models; using FileTime.Core.Timeline; namespace FileTime.Core.Command.CreateContainer; public class CreateContainerCommand : IExecutableCommand { - public Task CanRun(PointInTime currentTime) + private readonly ITimelessContentProvider _timelessContentProvider; + public FullName Parent { get; } + public string NewContainerName { get; } + + public CreateContainerCommand( + FullName parent, + string newContainerName, + ITimelessContentProvider timelessContentProvider) { - throw new NotImplementedException(); + _timelessContentProvider = timelessContentProvider; + Parent = parent; + NewContainerName = newContainerName; } - public Task SimulateCommand(PointInTime? currentTime) + public async Task CanRun(PointInTime currentTime) { - throw new NotImplementedException(); + try + { + var parent = await ResolveParentAsync(); + if (parent is not IContainer parentContainer) return CanCommandRun.False; + + var items = await parentContainer.Items.GetItemsAsync(); + if (items is null) return CanCommandRun.Forcable; + + var existingItem = items.FirstOrDefault(i => i.Path.GetName() == NewContainerName); + + return existingItem switch + { + null => CanCommandRun.True, + { Type: AbsolutePathType.Container } => CanCommandRun.Forcable, + _ => CanCommandRun.False + }; + } + catch + { + } + + return CanCommandRun.False; } - public Task Execute(ICommandScheduler commandScheduler) + public Task SimulateCommand(PointInTime currentTime) { - throw new NotImplementedException(); + return Task.FromResult( + currentTime.WithDifferences(newPointInTime => + new List() + { + new( + DifferenceActionType.Create, + new AbsolutePath(_timelessContentProvider, + newPointInTime, + Parent.GetChild(NewContainerName), + AbsolutePathType.Container + ) + ) + } + ) + ); } + + public Task Execute() + { + return Task.CompletedTask; + } + + private async Task ResolveParentAsync() + => await _timelessContentProvider.GetItemByFullNameAsync(Parent, PointInTime.Present); } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Command/FileTime.Core.Command.csproj b/src/Core/FileTime.Core.Command/FileTime.Core.Command.csproj index e75e348..17894cc 100644 --- a/src/Core/FileTime.Core.Command/FileTime.Core.Command.csproj +++ b/src/Core/FileTime.Core.Command/FileTime.Core.Command.csproj @@ -4,6 +4,10 @@ + + + + true diff --git a/src/Core/FileTime.Core.Services/Tab.cs b/src/Core/FileTime.Core.Services/Tab.cs index 865ac71..7baeaf9 100644 --- a/src/Core/FileTime.Core.Services/Tab.cs +++ b/src/Core/FileTime.Core.Services/Tab.cs @@ -109,4 +109,11 @@ public class Tab : ITab if (resolvedSelectedItem is not IContainer resolvedContainer) return; SetCurrentLocation(resolvedContainer); } + + public void Dispose() + { + _currentLocation.Dispose(); + _currentSelectedItem.Dispose(); + _itemFilters.Dispose(); + } } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Timeline/CommandRunnerContext.cs b/src/Core/FileTime.Core.Timeline/CommandRunnerContext.cs new file mode 100644 index 0000000..d8496bc --- /dev/null +++ b/src/Core/FileTime.Core.Timeline/CommandRunnerContext.cs @@ -0,0 +1,13 @@ +using FileTime.Core.Command; + +namespace FileTime.Core.Timeline; + +public class CommandRunnerContext +{ + public ICommand Command { get; } + + public CommandRunnerContext(ICommand command) + { + Command = command; + } +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Timeline/CommandScheduler.cs b/src/Core/FileTime.Core.Timeline/CommandScheduler.cs index 98d58bf..137598d 100644 --- a/src/Core/FileTime.Core.Timeline/CommandScheduler.cs +++ b/src/Core/FileTime.Core.Timeline/CommandScheduler.cs @@ -1,6 +1,165 @@ +using DynamicData; +using FileTime.Core.Command; +using Microsoft.Extensions.DependencyInjection; + namespace FileTime.Core.Timeline; public class CommandScheduler : ICommandScheduler { - + private readonly IServiceProvider _serviceProvider; + private readonly SourceList _commandsToRun = new(); + private readonly List _commandExecutors = new(); + + private readonly object _guard = new(); + private bool _enableRunning = true; + private bool _resourceIsInUse; + + public bool EnableRunning + { + get + { + bool result = true; + RunWithLock(() => result = _enableRunning); + return result; + } + + set { RunWithLock(() => _enableRunning = value); } + } + + public CommandScheduler(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + var localExecutor = serviceProvider.GetRequiredService(); + localExecutor.CommandFinished += LocalExecutorOnCommandFinished; + _commandExecutors.Add(localExecutor); + } + + public async Task AddCommand(ICommand command, int? batchId = null, bool toNewBatch = false) + { + await RunWithLockAsync(async () => + { + ParallelCommands batchToAdd; + + if (_commandsToRun.Count == 0) + { + batchToAdd = new ParallelCommands(PointInTime.CreateEmpty()); + _commandsToRun.Add(batchToAdd); + } + else if (toNewBatch) + { + batchToAdd = new ParallelCommands(_commandsToRun.Items.Last().Result); + _commandsToRun.Add(batchToAdd); + } + else if (batchId != null && _commandsToRun.Items.First(b => b.Id == batchId) is { } parallelCommands) + { + batchToAdd = parallelCommands; + } + else + { + batchToAdd = _commandsToRun.Items.First(); + } + + await batchToAdd.AddCommand(command); + + await RefreshCommands(); + + ExecuteCommands(); + + /*if (_commandRunners.Count == 0) + { + StartCommandRunner(); + } + + await UpdateReadOnlyCommands();*/ + }); + } + + private void ExecuteCommands() + { + if (!_enableRunning) return; + + var parallelCommandsToExecute = _commandsToRun.Items.FirstOrDefault(); + if (parallelCommandsToExecute is null || + parallelCommandsToExecute.Commands.All(c => c.ExecutionState != ExecutionState.Waiting)) return; + + var commandsToExecute = parallelCommandsToExecute.Commands; + + foreach (var commandToExecute in commandsToExecute) + { + if (commandToExecute.ExecutionState != ExecutionState.Waiting) continue; + + var commandExecutor = GetCommandExecutor(); + + commandExecutor.ExecuteCommand(commandToExecute.Command); + + commandToExecute.ExecutionState = ExecutionState.Running; + } + } + + private ICommandExecutor GetCommandExecutor() + { + //TODO + return _commandExecutors[0]; + } + + private void LocalExecutorOnCommandFinished(object? sender, ICommand command) + { + var parallelCommandsToExecute = _commandsToRun.Items.FirstOrDefault(); + if (parallelCommandsToExecute is null) return; + + var state = parallelCommandsToExecute.Commands.FirstOrDefault(c => c.Command == command); + if (state is null) return; + + state.ExecutionState = ExecutionState.Finished; + } + + private async Task RefreshCommands() + { + var currentTime = PointInTime.CreateEmpty(); + + foreach (var batch in _commandsToRun.Items) + { + currentTime = await batch.RefreshResult(currentTime); + } + } + + private void RunWithLock(Action action) => Task.Run(async () => await RunWithLockAsync(action)).Wait(); + + private async Task RunWithLockAsync(Action action) + { + await RunWithLockAsync(() => + { + action(); + return Task.CompletedTask; + }); + } + + private async Task RunWithLockAsync(Func func) + { + while (true) + { + lock (_guard) + { + if (!_resourceIsInUse) + { + _resourceIsInUse = true; + break; + } + } + + await Task.Delay(1); + } + + try + { + await func(); + } + finally + { + lock (_guard) + { + _resourceIsInUse = false; + } + } + } } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Timeline/LocalCommandExecutor.cs b/src/Core/FileTime.Core.Timeline/LocalCommandExecutor.cs new file mode 100644 index 0000000..bc9c859 --- /dev/null +++ b/src/Core/FileTime.Core.Timeline/LocalCommandExecutor.cs @@ -0,0 +1,34 @@ +using FileTime.Core.Command; + +namespace FileTime.Core.Timeline; + +public class LocalCommandExecutor : ILocalCommandExecutor +{ + private readonly ICommandRunner _commandRunner; + public event EventHandler CommandFinished; + + public LocalCommandExecutor(ICommandRunner commandRunner) + { + _commandRunner = commandRunner; + } + + public void ExecuteCommand(ICommand command) + { + var context = new CommandRunnerContext(command); + var thread = new Thread(new ParameterizedThreadStart(RunCommand)); + thread.Start(context); + } + + private async void RunCommand(object? contextObj) + { + if (contextObj is not CommandRunnerContext context) + throw new ArgumentException($"Parameter must be of type {typeof(CommandRunnerContext)}"); + try + { + await _commandRunner.RunCommandAsync(context.Command); + } + catch(Exception ex){} + + CommandFinished.Invoke(this, context.Command); + } +} \ No newline at end of file