diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Services/ISystemClipboardService.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Services/ISystemClipboardService.cs index b8c1f0e..67a6664 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Services/ISystemClipboardService.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Services/ISystemClipboardService.cs @@ -1,7 +1,9 @@ +using FileTime.Core.Models; + namespace FileTime.App.Core.Services; public interface ISystemClipboardService { Task CopyToClipboardAsync(string text); - Task GetFiles(); + Task> GetFiles(); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/UserCommand/PasteFilesFromClipboardCommand.cs b/src/AppCommon/FileTime.App.Core.Abstraction/UserCommand/PasteFilesFromClipboardCommand.cs index 5100f3b..6d28976 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/UserCommand/PasteFilesFromClipboardCommand.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/UserCommand/PasteFilesFromClipboardCommand.cs @@ -5,8 +5,12 @@ namespace FileTime.App.Core.UserCommand; public class PasteFilesFromClipboardCommand : IIdentifiableUserCommand { public const string PasteMergeCommandName = "paste_clipboard_merge"; + public const string PasteOverwriteCommandName = "paste_clipboard_overwrite"; + public const string PasteSkipCommandName = "paste_clipboard_skip"; public static readonly PasteFilesFromClipboardCommand Merge = new(PasteMode.Merge, PasteMergeCommandName); + public static readonly PasteFilesFromClipboardCommand Overwrite = new(PasteMode.Overwrite, PasteOverwriteCommandName); + public static readonly PasteFilesFromClipboardCommand Skip = new(PasteMode.Skip, PasteSkipCommandName); public PasteMode PasteMode { get; } private PasteFilesFromClipboardCommand(PasteMode pasteMode, string commandName) diff --git a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs index 888fa13..1743145 100644 --- a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs @@ -64,23 +64,38 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi AddCommandHandlers(new IUserCommandHandler[] { - new TypeUserCommandHandler(Copy), - new TypeUserCommandHandler(Delete), - new TypeUserCommandHandler(Rename), - new TypeUserCommandHandler(MarkItem), - new TypeUserCommandHandler(Paste), - new TypeUserCommandHandler(CreateContainer), - new TypeUserCommandHandler(CreateElement), - new TypeUserCommandHandler(PasteFilesFromClipboard), + new TypeUserCommandHandler(CopyAsync), + new TypeUserCommandHandler(DeleteAsync), + new TypeUserCommandHandler(RenameAsync), + new TypeUserCommandHandler(MarkItemAsync), + new TypeUserCommandHandler(PasteAsync), + new TypeUserCommandHandler(CreateContainerAsync), + new TypeUserCommandHandler(CreateElementAsync), + new TypeUserCommandHandler(PasteFilesFromClipboardAsync), }); } - private async Task PasteFilesFromClipboard(PasteFilesFromClipboardCommand arg) + private async Task PasteFilesFromClipboardAsync(PasteFilesFromClipboardCommand command) => + await (command.PasteMode switch + { + PasteMode.Merge => PasteFilesFromClipboardAsync(TransportMode.Merge), + PasteMode.Overwrite => PasteFilesFromClipboardAsync(TransportMode.Overwrite), + PasteMode.Skip => PasteFilesFromClipboardAsync(TransportMode.Skip), + _ => throw new ArgumentException($"Unknown {nameof(PasteMode)} value: {command.PasteMode}") + }); + + private async Task PasteFilesFromClipboardAsync(TransportMode mode) { - await _systemClipboardService.GetFiles(); + if (_currentLocation?.FullName is not { }) return; + + var files = (await _systemClipboardService.GetFiles()).ToList(); + var copyCommandFactory = _serviceProvider.GetRequiredService(); + var copyCommand = copyCommandFactory.GenerateCommand(files, mode, _currentLocation.FullName); + + await AddCommandAsync(copyCommand); } - private async Task MarkItem() + private async Task MarkItemAsync() { if (_selectedTab == null || _currentSelectedItem?.BaseItem?.FullName == null) return; @@ -88,7 +103,7 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi await _userCommandHandlerService.HandleCommandAsync(MoveCursorDownCommand.Instance); } - private Task Copy() + private Task CopyAsync() { _clipboardService.Clear(); _clipboardService.SetCommand(); @@ -114,18 +129,16 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi return Task.CompletedTask; } - private async Task Paste(PasteCommand command) - { + private async Task PasteAsync(PasteCommand command) => await (command.PasteMode switch { - PasteMode.Merge => Paste(TransportMode.Merge), - PasteMode.Overwrite => Paste(TransportMode.Overwrite), - PasteMode.Skip => Paste(TransportMode.Skip), + PasteMode.Merge => PasteAsync(TransportMode.Merge), + PasteMode.Overwrite => PasteAsync(TransportMode.Overwrite), + PasteMode.Skip => PasteAsync(TransportMode.Skip), _ => throw new ArgumentException($"Unknown {nameof(PasteMode)} value: {command.PasteMode}") }); - } - private async Task Paste(TransportMode mode) + private async Task PasteAsync(TransportMode mode) { if (_clipboardService.CommandFactoryType is null) { @@ -141,10 +154,10 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi if (command is IRequireInputCommand requireInput) await requireInput.ReadInputs(); - await AddCommand(command); + await AddCommandAsync(command); } - private async Task CreateContainer() + private async Task CreateContainerAsync() { var containerNameInput = new TextInputElement("Container name"); @@ -158,10 +171,10 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi var command = _serviceProvider .GetInitableResolver(_currentLocation.FullName, newContainerName) .GetRequiredService(); - await AddCommand(command); + await AddCommandAsync(command); } - private async Task CreateElement() + private async Task CreateElementAsync() { var containerNameInput = new TextInputElement("Element name"); @@ -175,10 +188,10 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi var command = _serviceProvider .GetInitableResolver(_currentLocation.FullName, newContainerName) .GetRequiredService(); - await AddCommand(command); + await AddCommandAsync(command); } - private async Task Rename(RenameCommand command) + private async Task RenameAsync(RenameCommand command) { List itemsToMove = new(); if ((_markedItems?.Collection?.Count ?? 0) > 0) @@ -331,7 +344,7 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi //TODO: check if the name changed var moveCommandFactory = _serviceProvider.GetRequiredService(); var moveCommand = moveCommandFactory.GenerateCommand(itemsToMove); - await AddCommand(moveCommand); + await AddCommandAsync(moveCommand); } static IEnumerable GetItemNameParts(Regex templateRegex, string originalName, string newNameSchema) @@ -368,7 +381,7 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi } } - private async Task Delete(DeleteCommand command) + private async Task DeleteAsync(DeleteCommand command) { IList? itemsToDelete = null; var shouldDelete = false; @@ -419,12 +432,12 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi var deleteCommand = _serviceProvider.GetRequiredService(); deleteCommand.HardDelete = command.IsHardDelete; deleteCommand.ItemsToDelete.AddRange(itemsToDelete!); - await AddCommand(deleteCommand); + await AddCommandAsync(deleteCommand); _selectedTab?.ClearMarkedItems(); } - private async Task AddCommand(ICommand command) + private async Task AddCommandAsync(ICommand command) { await _commandScheduler.AddCommand(command); } diff --git a/src/AppCommon/FileTime.App.Core/StartupServices/DefaultIdentifiableCommandHandlerRegister.cs b/src/AppCommon/FileTime.App.Core/StartupServices/DefaultIdentifiableCommandHandlerRegister.cs index 4f21604..b1d4219 100644 --- a/src/AppCommon/FileTime.App.Core/StartupServices/DefaultIdentifiableCommandHandlerRegister.cs +++ b/src/AppCommon/FileTime.App.Core/StartupServices/DefaultIdentifiableCommandHandlerRegister.cs @@ -41,6 +41,8 @@ public class DefaultIdentifiableCommandHandlerRegister : IStartupHandler AddUserCommand(PasteCommand.Overwrite); AddUserCommand(PasteCommand.Skip); AddUserCommand(PasteFilesFromClipboardCommand.Merge); + AddUserCommand(PasteFilesFromClipboardCommand.Overwrite); + AddUserCommand(PasteFilesFromClipboardCommand.Skip); AddUserCommand(PauseCommandSchedulerCommand.Instance); AddUserCommand(RefreshCommand.Instance); AddUserCommand(RenameCommand.Instance); diff --git a/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs b/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs index eff35b6..3042dde 100644 --- a/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs +++ b/src/AppCommon/FileTime.App.Search/SearchContentProvider.cs @@ -22,10 +22,11 @@ public class SearchContentProvider : ContentProviderBase, ISearchContentProvider AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown, ItemInitializationSettings itemInitializationSettings = default ) => - Task.FromResult((IItem)_searchManager.SearchTasks + Task.FromResult((IItem) _searchManager.SearchTasks .First(searchTask => searchTask.SearchContainer.NativePath == nativePath).SearchContainer); public override NativePath GetNativePath(FullName fullName) => new(fullName.Path); + public override FullName GetFullName(NativePath nativePath) => new(nativePath.Path); public override Task GetContentAsync( IElement element, diff --git a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProvider.cs b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProvider.cs index d7dc966..d5746ee 100644 --- a/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProvider.cs +++ b/src/Core/FileTime.Core.Abstraction/ContentAccess/IContentProvider.cs @@ -23,6 +23,7 @@ public interface IContentProvider : IContainer, IOnContainerEnter ItemInitializationSettings itemInitializationSettings = default); NativePath GetNativePath(FullName fullName); + FullName GetFullName(NativePath nativePath); Task GetContentAsync(IElement element, int? maxLength = null, CancellationToken cancellationToken = default); bool CanHandlePath(NativePath path); diff --git a/src/Core/FileTime.Core.Abstraction/Timeline/ITimelessContentProvider.cs b/src/Core/FileTime.Core.Abstraction/Timeline/ITimelessContentProvider.cs index dba7a69..616fc4b 100644 --- a/src/Core/FileTime.Core.Abstraction/Timeline/ITimelessContentProvider.cs +++ b/src/Core/FileTime.Core.Abstraction/Timeline/ITimelessContentProvider.cs @@ -15,4 +15,5 @@ public interface ITimelessContentProvider ItemInitializationSettings itemInitializationSettings = default); Task GetItemByNativePathAsync(NativePath nativePath, PointInTime? pointInTime = null); + FullName? GetFullNameByNativePath(NativePath nativePath); } \ No newline at end of file diff --git a/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs b/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs index 1307e53..7ad9e87 100644 --- a/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs +++ b/src/Core/FileTime.Core.ContentAccess/ContentProviderBase.cs @@ -88,6 +88,7 @@ public abstract class ContentProviderBase : IContentProvider ItemInitializationSettings itemInitializationSettings = default); public abstract NativePath GetNativePath(FullName fullName); + public abstract FullName GetFullName(NativePath nativePath); public abstract Task GetContentAsync(IElement element, int? maxLength = null, diff --git a/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs b/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs index 8823884..90c1e7a 100644 --- a/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs +++ b/src/Core/FileTime.Core.Timeline/TimelessContentProvider.cs @@ -44,4 +44,16 @@ public class TimelessContentProvider : ITimelessContentProvider return null; } + + public FullName? GetFullNameByNativePath(NativePath nativePath) + { + foreach (var contentProvider in _contentProviderRegistry.ContentProviders) + { + if(!contentProvider.CanHandlePath(nativePath)) continue; + + return contentProvider.GetFullName(nativePath); + } + + return null; + } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/MainConfiguration.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/MainConfiguration.cs index 1a6f59a..115fd74 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/MainConfiguration.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Configuration/MainConfiguration.cs @@ -77,7 +77,8 @@ public static class MainConfiguration new(PasteCommand.PasteMergeCommandName, new[] {Key.P, Key.P}), new(PasteCommand.PasteOverwriteCommandName, new[] {Key.P, Key.O}), new(PasteCommand.PasteSkipCommandName, new[] {Key.P, Key.S}), - new(PasteFilesFromClipboardCommand.PasteMergeCommandName, new[] {Key.C, Key.X}), + new(PasteFilesFromClipboardCommand.PasteMergeCommandName, new[] {new KeyConfig(Key.V, ctrl: true)}), + new(PasteFilesFromClipboardCommand.PasteOverwriteCommandName, new[] {new KeyConfig(Key.V, ctrl: true, shift: true)}), //new CommandBindingConfiguration(ConfigCommand.PinFavorite, new[] { Key.F, Key.P }), //new CommandBindingConfiguration(ConfigCommand.PreviousTimelineBlock, Key.H ), //new CommandBindingConfiguration(ConfigCommand.PreviousTimelineCommand, Key.K ), diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IUiAccessor.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IUiAccessor.cs new file mode 100644 index 0000000..0d66429 --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.Abstractions/Services/IUiAccessor.cs @@ -0,0 +1,10 @@ +using Avalonia.Controls; + +namespace FileTime.GuiApp.Services; + +public interface IUiAccessor +{ + public TopLevel? GetTopLevel(); + Task InvokeOnUIThread(Func func); + Task InvokeOnUIThread(Func> func); +} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Program.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Program.cs index 5a15adf..a8e24bc 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Program.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Program.cs @@ -3,6 +3,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; using Avalonia; using Avalonia.ReactiveUI; using Serilog; @@ -23,7 +26,7 @@ public static class Program InitRelease(); #endif InitLogging(); - + Log.Logger.Information("Early app starting..."); } @@ -71,7 +74,7 @@ public static class Program var logFolder = Path.Combine(AppDataRoot, "logs", "bootstrap"); if (!Directory.Exists(logFolder)) Directory.CreateDirectory(logFolder); - + Log.Logger = new LoggerConfiguration() .MinimumLevel.Verbose() .Enrich.FromLogContext() @@ -89,6 +92,9 @@ public static class Program [STAThread] public static void Main(string[] args) { + AppDomain.CurrentDomain.FirstChanceException -= OnFirstChanceException; + AppDomain.CurrentDomain.UnhandledException -= OnAppDomainUnhandledException; + TaskScheduler.UnobservedTaskException -= OnTaskSchedulerUnobservedTaskException; try { BuildAvaloniaApp() @@ -110,4 +116,22 @@ public static class Program .UsePlatformDetect() .UseReactiveUI() .LogToTrace(); + + private static void OnTaskSchedulerUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) + => HandleUnhandledException(sender, e.Exception); + + private static void OnAppDomainUnhandledException(object sender, UnhandledExceptionEventArgs e) + => HandleUnhandledException(sender, e.ExceptionObject as Exception); + + private static void OnFirstChanceException(object? sender, FirstChanceExceptionEventArgs e) + => HandleUnhandledException(sender, e.Exception); + + private static void HandleUnhandledException(object? sender, Exception? ex, [CallerMemberName] string caller = "") + => Log.Fatal( + ex, + "An unhandled exception come from '{Caller}' exception handler from an object of type '{Type}' and value '{Value}': {Exception}", + caller, + sender?.GetType().ToString() ?? "null", + sender?.ToString() ?? "null", + ex); } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Services/SystemClipboardService.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/SystemClipboardService.cs index d2ab155..e3d899d 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Services/SystemClipboardService.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Services/SystemClipboardService.cs @@ -1,35 +1,58 @@ -using Avalonia.Controls; +using System.Net; +using System.Text.Encodings.Web; +using Avalonia.Platform.Storage; using FileTime.App.Core.Services; +using FileTime.Core.Models; +using FileTime.Core.Timeline; namespace FileTime.GuiApp.Services; public class SystemClipboardService : ISystemClipboardService { - internal TopLevel? TopLevel { get; set; } + private const string ClipboardContentFiles = "Files"; + + private readonly ITimelessContentProvider _timelessContentProvider; + public IUiAccessor UiAccessor { get; internal set; } + + public SystemClipboardService(ITimelessContentProvider timelessContentProvider) + { + _timelessContentProvider = timelessContentProvider; + } + public async Task CopyToClipboardAsync(string text) { - var clipboard = TopLevel?.Clipboard; + var clipboard = UiAccessor.GetTopLevel()?.Clipboard; - if (clipboard is null) { return; } + if (clipboard is null) + { + return; + } await clipboard.SetTextAsync(text); } - public async Task GetFiles() + + public async Task> GetFiles() { - var clipboard = TopLevel?.Clipboard; + var clipboard = UiAccessor.GetTopLevel()?.Clipboard; - if (clipboard is null) { return; } + if (clipboard is null) + { + return Enumerable.Empty(); + } - await clipboard.ClearAsync(); + var formats = await UiAccessor.InvokeOnUIThread(async () => await clipboard.GetFormatsAsync()); - var formats = await clipboard.GetFormatsAsync(); + if (!formats.Contains(ClipboardContentFiles)) return Enumerable.Empty(); + var obj = await clipboard.GetDataAsync(ClipboardContentFiles); + + if (obj is IEnumerable storageItems) + { + return storageItems + .Select(i => _timelessContentProvider.GetFullNameByNativePath(new NativePath(WebUtility.UrlDecode(i.Path.AbsolutePath)))) + .Where(i => i != null) + .OfType(); + } - if (!formats.Contains("asd")) return; - var obj = (await clipboard.GetDataAsync("PNG")); - /*var ms = new MemoryStream(); - Serializer.Serialize(ms, obj); - byte[] data = ms.ToArray().Skip(4).ToArray(); - ms = new MemoryStream(data);*/ - ; + return Enumerable.Empty(); } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml.cs index 63de738..4029b41 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml.cs @@ -1,19 +1,21 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Threading; using Avalonia.VisualTree; using DynamicData; using FileTime.App.Core.Services; using FileTime.App.Core.ViewModels; using FileTime.Core.Models; using FileTime.GuiApp.Models; +using FileTime.GuiApp.Services; using FileTime.GuiApp.ViewModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace FileTime.GuiApp.Views; -public partial class MainWindow : Window +public partial class MainWindow : Window, IUiAccessor { private readonly ILogger? _logger; private readonly IModalService _modalService; @@ -39,7 +41,7 @@ public partial class MainWindow : Window _logger?.LogInformation($"Starting {nameof(MainWindow)} initialization..."); _modalService = DI.ServiceProvider.GetRequiredService(); _modalService.OpenModals.ToCollection().Subscribe(m => _openModals = m); - DI.ServiceProvider.GetRequiredService().TopLevel = GetTopLevel(this); + DI.ServiceProvider.GetRequiredService().UiAccessor = this; InitializeComponent(); ReadInputContainer.PropertyChanged += ReadInputContainerOnPropertyChanged; @@ -156,4 +158,10 @@ public partial class MainWindow : Window _inputViewModel = null; } } + + public TopLevel? GetTopLevel() => GetTopLevel(this); + + public async Task InvokeOnUIThread(Func func) => await Dispatcher.UIThread.InvokeAsync(func); + + public async Task InvokeOnUIThread(Func> func) => await Dispatcher.UIThread.InvokeAsync(func); } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs index d9bd09e..aec7ba5 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs @@ -338,7 +338,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo private FullName GetFullName(DirectoryInfo directoryInfo) => GetFullName(directoryInfo.FullName); private FullName GetFullName(FileInfo fileInfo) => GetFullName(fileInfo.FullName); - private FullName GetFullName(NativePath nativePath) => GetFullName(nativePath.Path); + public override FullName GetFullName(NativePath nativePath) => GetFullName(nativePath.Path); private FullName GetFullName(string nativePath) => FullName.CreateSafe((Name + Constants.SeparatorChar +