From c061f658aa54279d7aa0dfd3f1748b2f3d632c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Sat, 5 Feb 2022 00:04:27 +0100 Subject: [PATCH] Optimization, UI improvements, Run command --- .../FileTime.App.Core/Tab/TabState.cs | 2 +- .../Application.CommandHandlers.cs | 2 +- .../FileTime.Core/Command/RenameCommand.cs | 2 +- src/Core/FileTime.Core/Components/Tab.cs | 18 +- src/Core/FileTime.Core/Models/IElement.cs | 2 + src/Core/FileTime.Core/Models/IItem.cs | 3 +- .../FileTime.Core/Models/VirtualContainer.cs | 7 + .../FileTime.Core/Providers/TopContainer.cs | 6 +- .../FileTime.Core/Timeline/TimeContainer.cs | 4 + .../FileTime.Core/Timeline/TimeElement.cs | 7 + .../FileTime.Core/Timeline/TimeProvider.cs | 4 + src/GuiApp/FileTime.Avalonia/App.axaml | 6 + .../Application/TabContainer.cs | 91 +++++-- .../Converters/CompareConverter.cs | 9 +- .../Converters/IsElementConverter.cs | 17 ++ .../Misc/InputElementWrapper.cs | 7 +- .../Models/ElementPreviewMode.cs | 9 + src/GuiApp/FileTime.Avalonia/Program.cs | 7 + .../ViewModels/ContainerViewModel.cs | 91 +++++-- .../ViewModels/ElementPreviewViewModel.cs | 46 ++++ .../ViewModels/MainPageViewModel.cs | 98 +++++-- .../FileTime.Avalonia/Views/MainWindow.axaml | 256 ++++++++++-------- .../Views/MainWindow.axaml.cs | 14 +- .../LocalContentProvider.cs | 4 + .../FileTime.Providers.Local/LocalFile.cs | 7 + .../FileTime.Providers.Local/LocalFolder.cs | 32 ++- .../SmbContentProvider.cs | 4 + .../FileTime.Providers.Smb/SmbFile.cs | 6 + .../FileTime.Providers.Smb/SmbFolder.cs | 12 + .../FileTime.Providers.Smb/SmbServer.cs | 6 +- .../FileTime.Providers.Smb/SmbShare.cs | 12 + 31 files changed, 579 insertions(+), 212 deletions(-) create mode 100644 src/GuiApp/FileTime.Avalonia/Converters/IsElementConverter.cs create mode 100644 src/GuiApp/FileTime.Avalonia/Models/ElementPreviewMode.cs create mode 100644 src/GuiApp/FileTime.Avalonia/ViewModels/ElementPreviewViewModel.cs diff --git a/src/AppCommon/FileTime.App.Core/Tab/TabState.cs b/src/AppCommon/FileTime.App.Core/Tab/TabState.cs index 6615669..466e9b5 100644 --- a/src/AppCommon/FileTime.App.Core/Tab/TabState.cs +++ b/src/AppCommon/FileTime.App.Core/Tab/TabState.cs @@ -101,7 +101,7 @@ namespace FileTime.App.Core.Tab : new List().AsReadOnly(); } - public async Task MakrCurrentItem() + public async Task MarkCurrentItem() { var currentLocation = await Tab!.GetCurrentLocation(); if (currentLocation != null) diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Application.CommandHandlers.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Application.CommandHandlers.cs index 8cf77fe..e7793e3 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Application.CommandHandlers.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Application.CommandHandlers.cs @@ -67,7 +67,7 @@ namespace FileTime.ConsoleUI.App { if (_selectedTab != null) { - await _tabStates[_selectedTab].MakrCurrentItem(); + await _tabStates[_selectedTab].MarkCurrentItem(); } } diff --git a/src/Core/FileTime.Core/Command/RenameCommand.cs b/src/Core/FileTime.Core/Command/RenameCommand.cs index ccb815f..959c152 100644 --- a/src/Core/FileTime.Core/Command/RenameCommand.cs +++ b/src/Core/FileTime.Core/Command/RenameCommand.cs @@ -27,7 +27,7 @@ namespace FileTime.Core.Command if (itemToRename != null) { await itemToRename.Rename(Target); - timeRunner.RefreshContainer?.InvokeAsync(this, new AbsolutePath(itemToRename.GetParent()!)); + if(timeRunner.RefreshContainer != null) await timeRunner.RefreshContainer.InvokeAsync(this, new AbsolutePath(itemToRename.GetParent()!)); } } diff --git a/src/Core/FileTime.Core/Components/Tab.cs b/src/Core/FileTime.Core/Components/Tab.cs index f0bcabc..9c9bf6d 100644 --- a/src/Core/FileTime.Core/Components/Tab.cs +++ b/src/Core/FileTime.Core/Components/Tab.cs @@ -11,7 +11,9 @@ namespace FileTime.Core.Components #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. private string? _lastPath; - private readonly object _guardSetCurrentSelectedItemCTS = new object(); + private bool _currentlySelecting = false; + + private readonly object _guardSetCurrentSelectedItemCTS = new(); private CancellationTokenSource? _setCurrentSelectedItemCTS; public int CurrentSelectedIndex { get; private set; } @@ -54,6 +56,8 @@ namespace FileTime.Core.Components public async Task SetCurrentSelectedItem(IItem? value, bool secondary = false) { + if (_currentlySelecting) return; + if (_currentSelectedItem != value) { IItem? itemToSelect = null; @@ -190,11 +194,13 @@ namespace FileTime.Core.Components { if (!currentPossibleItems.Any()) return; - var currentLocationItems = (await (await GetCurrentLocation()).GetItems())!; + var currentLocation = await GetCurrentLocation(); + var currentLocationItems = (await currentLocation.GetItems())!; if (await GetCurrentSelectedItem() != null) { - (await GetCurrentLocation())?.RefreshAsync(); + _currentlySelecting = true; + currentLocation?.RefreshAsync(); IItem? newSelectedItem = null; foreach (var item in currentPossibleItems) @@ -212,6 +218,7 @@ namespace FileTime.Core.Components newSelectedItem = (await (await GetCurrentLocation()).GetItems())?.FirstOrDefault(i => i.Name == newSelectedItem.Name); } + _currentlySelecting = false; await SetCurrentSelectedItem(newSelectedItem ?? (currentLocationItems.Count > 0 ? currentLocationItems[0] : null)); } else @@ -261,6 +268,11 @@ namespace FileTime.Core.Components var newCurrentLocation = (await (await GetCurrentLocation()).GetItems())?.FirstOrDefault(i => i.Name == lastCurrentLocation.Name); await SetCurrentSelectedItem(newCurrentLocation); } + + foreach(var lastLocationItem in currentLocationItems.OfType()) + { + lastLocationItem.Dispose(); + } } } diff --git a/src/Core/FileTime.Core/Models/IElement.cs b/src/Core/FileTime.Core/Models/IElement.cs index e8fb630..cc3c0b2 100644 --- a/src/Core/FileTime.Core/Models/IElement.cs +++ b/src/Core/FileTime.Core/Models/IElement.cs @@ -4,5 +4,7 @@ namespace FileTime.Core.Models { bool IsSpecial { get; } string GetPrimaryAttributeText(); + Task GetContent(CancellationToken token = default); + Task GetElementSize(CancellationToken token = default); } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Models/IItem.cs b/src/Core/FileTime.Core/Models/IItem.cs index 1c5c3e9..a462702 100644 --- a/src/Core/FileTime.Core/Models/IItem.cs +++ b/src/Core/FileTime.Core/Models/IItem.cs @@ -2,11 +2,12 @@ using FileTime.Core.Providers; namespace FileTime.Core.Models { - public interface IItem + public interface IItem : IDisposable { string Name { get; } string? FullName { get; } bool IsHidden { get; } + bool IsDisposed { get; } SupportsDelete CanDelete { get; } bool CanRename { get; } IContentProvider Provider { get; } diff --git a/src/Core/FileTime.Core/Models/VirtualContainer.cs b/src/Core/FileTime.Core/Models/VirtualContainer.cs index 0546e54..3efeb44 100644 --- a/src/Core/FileTime.Core/Models/VirtualContainer.cs +++ b/src/Core/FileTime.Core/Models/VirtualContainer.cs @@ -35,6 +35,8 @@ namespace FileTime.Core.Models public AsyncEventHandler Refreshed { get; } + public bool IsDisposed => BaseContainer.IsDisposed; + private void RefreshAddBase(Func handler) { BaseContainer.Refreshed.Add(handler); @@ -168,5 +170,10 @@ namespace FileTime.Core.Models public async Task Rename(string newName) => await BaseContainer.Rename(newName); public async Task CanOpen() => await BaseContainer.CanOpen(); + + public void Dispose() + { + BaseContainer.Dispose(); + } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Providers/TopContainer.cs b/src/Core/FileTime.Core/Providers/TopContainer.cs index ce9e2b7..b0f5b5d 100644 --- a/src/Core/FileTime.Core/Providers/TopContainer.cs +++ b/src/Core/FileTime.Core/Providers/TopContainer.cs @@ -33,6 +33,8 @@ namespace FileTime.Core.Providers public bool SupportsDirectoryLevelSoftDelete => false; + public bool IsDisposed => false; + public TopContainer(IEnumerable contentProviders) { _contentProviders = new List(contentProviders); @@ -57,7 +59,7 @@ namespace FileTime.Core.Providers public Task IsExists(string name) => throw new NotImplementedException(); - public async Task RefreshAsync(CancellationToken token = default) => await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty); + public async Task RefreshAsync(CancellationToken token = default) => await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty, token); public Task?> GetItems(CancellationToken token = default) => Task.FromResult(_items); public Task?> GetContainers(CancellationToken token = default) => Task.FromResult(_containers); @@ -68,5 +70,7 @@ namespace FileTime.Core.Providers public Task Rename(string newName) => throw new NotSupportedException(); public Task CanOpen() => Task.FromResult(true); + + public void Dispose() { } } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/TimeContainer.cs b/src/Core/FileTime.Core/Timeline/TimeContainer.cs index 1221e07..a155a72 100644 --- a/src/Core/FileTime.Core/Timeline/TimeContainer.cs +++ b/src/Core/FileTime.Core/Timeline/TimeContainer.cs @@ -29,6 +29,8 @@ namespace FileTime.Core.Timeline public bool SupportsDirectoryLevelSoftDelete => false; + public bool IsDisposed { get; private set; } + public TimeContainer(string name, IContainer parent, IContentProvider contentProvider, IContentProvider virtualContentProvider, PointInTime pointInTime) { _parent = parent; @@ -121,5 +123,7 @@ namespace FileTime.Core.Timeline return new TimeElement(elementDiff.Name, this, Provider, elementDiff.AbsolutePath.VirtualContentProvider ?? elementDiff.AbsolutePath.ContentProvider); } public Task CanOpen() => Task.FromResult(true); + + public void Dispose() => IsDisposed = true; } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/TimeElement.cs b/src/Core/FileTime.Core/Timeline/TimeElement.cs index dd8eef4..08510a2 100644 --- a/src/Core/FileTime.Core/Timeline/TimeElement.cs +++ b/src/Core/FileTime.Core/Timeline/TimeElement.cs @@ -31,6 +31,8 @@ namespace FileTime.Core.Timeline public IContentProvider Provider { get; } public IContentProvider VirtualProvider { get; } + public bool IsDisposed { get; private set; } + public Task Delete(bool hardDelete = false) => Task.CompletedTask; public IContainer? GetParent() => _parent; @@ -38,5 +40,10 @@ namespace FileTime.Core.Timeline public string GetPrimaryAttributeText() => ""; public Task Rename(string newName) => Task.CompletedTask; + + public Task GetContent(CancellationToken token = default) => Task.FromResult(""); + public Task GetElementSize(CancellationToken token = default) => Task.FromResult(-1L); + + public void Dispose() => IsDisposed = true; } } \ No newline at end of file diff --git a/src/Core/FileTime.Core/Timeline/TimeProvider.cs b/src/Core/FileTime.Core/Timeline/TimeProvider.cs index f7aadc3..87442e8 100644 --- a/src/Core/FileTime.Core/Timeline/TimeProvider.cs +++ b/src/Core/FileTime.Core/Timeline/TimeProvider.cs @@ -28,6 +28,8 @@ namespace FileTime.Core.Timeline public bool SupportsDirectoryLevelSoftDelete => false; + public bool IsDisposed => false; + public TimeProvider(PointInTime pointInTime) { _pointInTime = pointInTime; @@ -90,5 +92,7 @@ namespace FileTime.Core.Timeline public void SetParent(IContainer container) { } public Task CanOpen() => Task.FromResult(true); + + public void Dispose() { } } } \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/App.axaml b/src/GuiApp/FileTime.Avalonia/App.axaml index 28c5bb2..88d3774 100644 --- a/src/GuiApp/FileTime.Avalonia/App.axaml +++ b/src/GuiApp/FileTime.Avalonia/App.axaml @@ -12,6 +12,7 @@ #E7073642 #083e4c #D0083e4c + #80083e4c #00000000 #10000000 @@ -128,6 +129,7 @@ + @@ -145,6 +147,10 @@ + diff --git a/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs b/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs index 4410190..458e6cf 100644 --- a/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs +++ b/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs @@ -20,6 +20,7 @@ namespace FileTime.Avalonia.Application [Inject(typeof(Tab))] public partial class TabContainer : INewItemProcessor { + private bool _updateFromCode; [Property] private TabState _tabState; @@ -46,28 +47,44 @@ namespace FileTime.Avalonia.Application get => _selectedItem; set { - if (value != null) + if (!_updateFromCode && value != null) { try { - SetSelectedItemAsync(value, true).Wait(); + /*var task = SetSelectedItemAsync(value, true); + Task.WaitAll(new Task[] { task }, 100);*/ + SetSelectedItemAsync(value, true); } - catch - { + catch + { //TODO: Debug, linux start after restore 3 tabs } } } } + [Property] + private ElementPreviewViewModel? _elementPreview; + public async Task SetSelectedItemAsync(IItemViewModel? value, bool fromDataBinding = false) { if (_selectedItem != value) { _selectedItem = value; - OnPropertyChanged(nameof(SelectedItem)); + if (value is ElementViewModel elementViewModel) + { + var elementPreview = new ElementPreviewViewModel(); + await elementPreview.Init(elementViewModel.Element); + ElementPreview = elementPreview; + } + else + { + ElementPreview = null; + } + await Tab.SetCurrentSelectedItem(SelectedItem?.Item, fromDataBinding); + OnPropertyChanged(nameof(SelectedItem)); } } @@ -174,7 +191,7 @@ namespace FileTime.Avalonia.Application if (token.IsCancellationRequested) return; - var items = await _currentLocation.GetItems(); + var items = await _currentLocation.GetItems(token); if (items?.Count > 0) { foreach (var item in items) @@ -198,7 +215,7 @@ namespace FileTime.Avalonia.Application { if (token.IsCancellationRequested) return; var activeChildItem = await Tab.GetItemByLastPath(containerViewModel.Container); - child = (await containerViewModel.GetItems()).FirstOrDefault(i => i.Item == activeChildItem); + child = (await containerViewModel.GetItems(token)).FirstOrDefault(i => i.Item == activeChildItem); if (child != null) { child.IsSelected = true; @@ -239,59 +256,71 @@ namespace FileTime.Avalonia.Application catch { } } + private async Task RunFromCode(Func task) + { + _updateFromCode = true; + try + { + await task(); + } + catch + { + _updateFromCode = false; + throw; + } + } + public async Task Open() { if (ChildContainer != null) { - await Tab.Open(); - await UpdateCurrentSelectedItem(); + await RunFromCode(Tab.Open); } } public async Task GoUp() { - await Tab.GoUp(); - await UpdateCurrentSelectedItem(); + await RunFromCode(Tab.GoUp); } public async Task MoveCursorDown() { - await Tab.SelectNextItem(); + await RunFromCode(async () => await Tab.SelectNextItem()); } public async Task MoveCursorDownPage() { - await Tab.SelectNextItem(10); + await RunFromCode(async () => await Tab.SelectNextItem(10)); } public async Task MoveCursorUp() { - await Tab.SelectPreviousItem(); + await RunFromCode(async () => await Tab.SelectPreviousItem()); } public async Task MoveCursorUpPage() { - await Tab.SelectPreviousItem(10); + await RunFromCode(async () => await Tab.SelectPreviousItem(10)); } public async Task MoveCursorToFirst() { - await Tab.SelectFirstItem(); + await RunFromCode(Tab.SelectFirstItem); } public async Task MoveCursorToLast() { - await Tab.SelectLastItem(); + await RunFromCode(Tab.SelectLastItem); } public async Task GotToProvider() { - await Tab.GoToProvider(); + await RunFromCode(Tab.GoToProvider); } public async Task GotToRoot() { - await Tab.GoToRoot(); + await RunFromCode(Tab.GoToRoot); } public async Task GotToHome() @@ -300,28 +329,42 @@ namespace FileTime.Avalonia.Application var resolvedPath = await LocalContentProvider.GetByPath(path); if (resolvedPath is IContainer homeFolder) { - await Tab.OpenContainer(homeFolder); + await OpenContainer(homeFolder); } } public async Task CreateContainer(string name) { - (await Tab.GetCurrentLocation())?.CreateContainer(name); + await RunFromCode(async () => + { + var currentLocation = await Tab.GetCurrentLocation(); + if (currentLocation != null) + { + await currentLocation.CreateContainer(name); + } + }); } public async Task CreateElement(string name) { - (await Tab.GetCurrentLocation())?.CreateElement(name); + await RunFromCode(async () => + { + var currentLocation = await Tab.GetCurrentLocation(); + if (currentLocation != null) + { + await currentLocation.CreateElement(name); + } + }); } public async Task OpenContainer(IContainer container) { - await Tab.OpenContainer(container); + await RunFromCode(async () => await Tab.OpenContainer(container)); } public async Task MarkCurrentItem() { - await _tabState.MakrCurrentItem(); + await _tabState.MarkCurrentItem(); } public async Task UpdateMarkedItems(ContainerViewModel containerViewModel, CancellationToken token = default) diff --git a/src/GuiApp/FileTime.Avalonia/Converters/CompareConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/CompareConverter.cs index 80cee32..95880bd 100644 --- a/src/GuiApp/FileTime.Avalonia/Converters/CompareConverter.cs +++ b/src/GuiApp/FileTime.Avalonia/Converters/CompareConverter.cs @@ -42,11 +42,16 @@ namespace FileTime.Avalonia.Converters else if (value is double valueDouble && (parameter is double parameterDouble || double.TryParse(parameter?.ToString(), out parameterDouble))) return valueDouble != parameterDouble; return value != parameter; } - if (ComparisonCondition == ComparisonCondition.Equal) + else if (ComparisonCondition == ComparisonCondition.Equal) { if (value is int valueInt && (parameter is int parameterInt || int.TryParse(parameter?.ToString(), out parameterInt))) return valueInt == parameterInt; else if (value is double valueDouble && (parameter is double parameterDouble || double.TryParse(parameter?.ToString(), out parameterDouble))) return valueDouble == parameterDouble; - else if (value?.GetType().IsEnum ?? false && Enum.TryParse(value.GetType(), parameter?.ToString(), out var _)) return value.ToString() == parameter?.ToString(); + else if (value?.GetType().IsEnum ?? false && Enum.TryParse(value.GetType(), parameter?.ToString(), out var _)) + { + var s1 = value.ToString(); + var s2 = parameter?.ToString(); + return s1 == s2; + } } return value == parameter; diff --git a/src/GuiApp/FileTime.Avalonia/Converters/IsElementConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/IsElementConverter.cs new file mode 100644 index 0000000..faa2d69 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Converters/IsElementConverter.cs @@ -0,0 +1,17 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using FileTime.Core.Models; + +namespace FileTime.Avalonia.Converters +{ + public class IsElementConverter : IValueConverter + { + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => value is IElement; + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Misc/InputElementWrapper.cs b/src/GuiApp/FileTime.Avalonia/Misc/InputElementWrapper.cs index 33f2669..f81ce59 100644 --- a/src/GuiApp/FileTime.Avalonia/Misc/InputElementWrapper.cs +++ b/src/GuiApp/FileTime.Avalonia/Misc/InputElementWrapper.cs @@ -1,21 +1,20 @@ using FileTime.Core.Interactions; -using System; -using System.Collections.Generic; -using System.Text; namespace FileTime.Avalonia.Misc { public class InputElementWrapper { - public InputElement InputElement { get; } public string Value { get; set; } + public char? PasswordChar { get; set; } + public InputElementWrapper(InputElement inputElement, string? defaultValue = null) { InputElement = inputElement; Value = defaultValue ?? ""; + PasswordChar = inputElement.InputType == InputType.Password ? '*' : null; } } } diff --git a/src/GuiApp/FileTime.Avalonia/Models/ElementPreviewMode.cs b/src/GuiApp/FileTime.Avalonia/Models/ElementPreviewMode.cs new file mode 100644 index 0000000..627971d --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Models/ElementPreviewMode.cs @@ -0,0 +1,9 @@ +namespace FileTime.Avalonia.Models +{ + public enum ElementPreviewMode + { + Unknown, + Text, + Empty + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Program.cs b/src/GuiApp/FileTime.Avalonia/Program.cs index aff7bea..dce3676 100644 --- a/src/GuiApp/FileTime.Avalonia/Program.cs +++ b/src/GuiApp/FileTime.Avalonia/Program.cs @@ -60,6 +60,12 @@ namespace FileTime.Avalonia { BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } +#if DEBUG + catch + { + throw; + } +#else catch (Exception e) { var message = $"Ciritcal error cought in {nameof(Program)}"; @@ -79,6 +85,7 @@ namespace FileTime.Avalonia using var streamWriter = new StreamWriter(fileWriter); streamWriter.WriteLine(DateTime.Now.ToString() + ": " + message + "\n" + e.ToString() + "\n\n"); } +#endif } // Avalonia configuration, don't remove; also used by visual designer. diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs index ce05175..8cf97c3 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs @@ -133,12 +133,13 @@ namespace FileTime.Avalonia.ViewModels public async Task Init(bool initializeChildren = true, CancellationToken token = default) { - await Refresh(initializeChildren, token); + if (_isInitialized) return; + await Refresh(initializeChildren, token: token); } private async Task Container_Refreshed(object? sender, AsyncEventArgs e, CancellationToken token = default) { - await Refresh(false, token); + await Refresh(false, false, token); } [Obsolete($"Use the parametrizable version of {nameof(Refresh)}.")] @@ -146,7 +147,7 @@ namespace FileTime.Avalonia.ViewModels { await Refresh(true); } - private async Task Refresh(bool initializeChildren, CancellationToken token = default) + private async Task Refresh(bool initializeChildren, bool alloweReuse = true, CancellationToken token = default) { if (_isRefreshing) return; @@ -157,18 +158,13 @@ namespace FileTime.Avalonia.ViewModels { _isRefreshing = true; - var containers = (await _container.GetContainers())!.Select(c => AdoptOrReuseOrCreateItem(c, (c2) => new ContainerViewModel(_newItemProcessor, this, c2, ItemNameConverterService))).ToList(); - var elements = (await _container.GetElements())!.Select(e => AdoptOrReuseOrCreateItem(e, (e2) => new ElementViewModel(e2, this, ItemNameConverterService))).ToList(); + var containers = (await _container.GetContainers())!.Select(c => AdoptOrReuseOrCreateItem(c, alloweReuse , (c2) => new ContainerViewModel(_newItemProcessor, this, c2, ItemNameConverterService))).ToList(); + var elements = (await _container.GetElements())!.Select(e => AdoptOrReuseOrCreateItem(e, alloweReuse , (e2) => new ElementViewModel(e2, this, ItemNameConverterService))).ToList(); if (token.IsCancellationRequested) return; Exceptions = new List(_container.Exceptions); - foreach (var containerToRemove in _containers.Except(containers)) - { - containerToRemove?.Dispose(); - } - if (initializeChildren) { foreach (var container in containers) @@ -178,14 +174,53 @@ namespace FileTime.Avalonia.ViewModels } } - for (var i = 0; i < _items.Count; i++) + /*var containersToAdd = containers.Except(_containers).ToList(); + var containersToRemove = _containers.Except(containers).ToList(); + + var elementsToAdd = elements.Except(_elements).ToList(); + var elementsToRemove = _elements.Except(elements).ToList(); + + foreach (var containerToRemove in containersToRemove) { - _items[i].IsAlternative = i % 2 == 1; + Containers.Remove(containerToRemove); + Items.Remove(containerToRemove); + containerToRemove?.Dispose(); + } + + foreach (var elementToRemove in elementsToRemove) + { + Elements.Remove(elementToRemove); + Items.Remove(elementToRemove); + } + + foreach (var containerToAdd in containersToAdd) + { + Containers.Insert(GetNewItemPosition(containerToAdd, Containers), containerToAdd); + Items.Insert(GetNewItemPosition(containerToAdd, Items), containerToAdd); + } + + foreach (var elementToAdd in elementsToAdd) + { + Elements.Insert(GetNewItemPosition(elementToAdd, Elements), elementToAdd); + Items.Insert(GetNewItemPosition(elementToAdd, Items), elementToAdd); + }*/ + + + + var containersToRemove = _containers.Except(containers).ToList(); + foreach (var containerToRemove in containersToRemove) + { + containerToRemove?.Dispose(); } Containers = new ObservableCollection(containers); Elements = new ObservableCollection(elements); Items = new ObservableCollection(containers.Cast().Concat(elements)); + + for (var i = 0; i < Items.Count; i++) + { + Items[i].IsAlternative = i % 2 == 1; + } } catch (Exception e) { @@ -197,13 +232,31 @@ namespace FileTime.Avalonia.ViewModels _isRefreshing = false; } - private TResult AdoptOrReuseOrCreateItem(T item, Func generator) where T : class, IItem + private int GetNewItemPosition(TItem itemToAdd, IList items) where TItem : IItemViewModel where T : IItemViewModel + { + var i = 0; + for (; i < items.Count; i++) + { + var item = items[i]; + if (item is TItem && itemToAdd.Item.Name.CompareTo(item.Item.Name) < 0) + { + return i - 1; + } + } + + return i; + } + + private TResult AdoptOrReuseOrCreateItem(T item, bool allowResuse, Func generator) where T : class, IItem { var itemToAdopt = ChildrenToAdopt.Find(i => i.Item == item); if (itemToAdopt is TResult itemViewModel) return itemViewModel; + if (allowResuse) + { var existingViewModel = _items?.FirstOrDefault(i => i.Item == item); if (existingViewModel is TResult itemViewModelToReuse) return itemViewModelToReuse; + } return generator(item); } @@ -221,26 +274,26 @@ namespace FileTime.Avalonia.ViewModels } } - _containers = new ObservableCollection(); - _elements = new ObservableCollection(); - _items = new ObservableCollection(); + _containers.Clear(); + _elements.Clear(); + _items.Clear(); } public async Task> GetContainers(CancellationToken token = default) { - if (!_isInitialized) await Task.Run(async () => await Refresh(false, token), token); + if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token); return _containers; } public async Task> GetElements(CancellationToken token = default) { - if (!_isInitialized) await Task.Run(async () => await Refresh(false, token), token); + if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token); return _elements; } public async Task> GetItems(CancellationToken token = default) { - if (!_isInitialized) await Task.Run(async () => await Refresh(false, token), token); + if (!_isInitialized) await Task.Run(async () => await Refresh(false, token: token), token); return _items; } diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ElementPreviewViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ElementPreviewViewModel.cs new file mode 100644 index 0000000..488de73 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ElementPreviewViewModel.cs @@ -0,0 +1,46 @@ +using FileTime.Avalonia.Models; +using FileTime.Core.Models; +using MvvmGen; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace FileTime.Avalonia.ViewModels +{ + [ViewModel] + public partial class ElementPreviewViewModel + { + private const int MAXTEXTPREVIEWSIZE = 1024 * 1024; + [Property] + private IElement? _element; + + [Property] + private string? _textContent; + + [Property] + private ElementPreviewMode? _mode; + + public async Task Init(IElement element, CancellationToken token = default) + { + Element = element; + + var elementSize = await element.GetElementSize(token); + if (elementSize == 0) + { + Mode = ElementPreviewMode.Empty; + } + else if (elementSize < MAXTEXTPREVIEWSIZE) + { + TextContent = await element.GetContent(); + Mode = ElementPreviewMode.Text; + } + else + { + Mode = ElementPreviewMode.Unknown; + } + } + } +} diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs index 910bc48..d83508d 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs @@ -74,10 +74,10 @@ namespace FileTime.Avalonia.ViewModels private string _messageBoxText; [Property] - private ObservableCollection _popupTexts = new ObservableCollection(); + private ObservableCollection _popupTexts = new(); [Property] - private bool _showAllShortcut; + private bool _isAllShortcutVisible; [Property] private List _allShortcut; @@ -127,7 +127,7 @@ namespace FileTime.Avalonia.ViewModels var driveInfos = new List(); var drives = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DriveInfo.GetDrives().Where(d => d.DriveType == DriveType.Fixed) - : DriveInfo.GetDrives().Where(d => + : DriveInfo.GetDrives().Where(d => d.DriveType == DriveType.Fixed && d.DriveFormat != "pstorefs" && d.DriveFormat != "bpf_fs" @@ -716,9 +716,58 @@ namespace FileTime.Avalonia.ViewModels } } - private Task ShowAllShortcut2() + private Task ShowAllShortcut() { - ShowAllShortcut = true; + IsAllShortcutVisible = true; + return Task.CompletedTask; + } + + private Task RunCommandInContainer() + { + var handler = () => + { + if (Inputs != null) + { + var input = Inputs[0].Value; + string? path = null; + string? arguments = null; + + if (input.StartsWith("\"")) + { + var pathEnd = input.IndexOf('\"', 1); + + path = input.Substring(1, pathEnd); + arguments = input.Substring(pathEnd + 1).Trim(); + } + else + { + var inputParts = input.Split(' '); + path = inputParts[0]; + arguments = inputParts.Length > 1 ? string.Join(' ', inputParts[1..]).Trim() : null; + } + + if (!string.IsNullOrWhiteSpace(path)) + { + var process = new Process(); + process.StartInfo.FileName = path; + + if (!string.IsNullOrWhiteSpace(arguments)) + { + process.StartInfo.Arguments = arguments; + } + if (AppState.SelectedTab.CurrentLocation.Container is LocalFolder localFolder) + { + process.StartInfo.WorkingDirectory = localFolder.Directory.FullName; + } + process.Start(); + } + } + + return Task.CompletedTask; + }; + + ReadInputs(new List() { new Core.Interactions.InputElement("Command", InputType.Text) }, handler); + return Task.CompletedTask; } @@ -757,14 +806,14 @@ namespace FileTime.Avalonia.ViewModels _inputHandler = null; } - public async Task ProcessKeyDown(Key key, KeyModifiers keyModifiers) + public async void ProcessKeyDown(Key key, KeyModifiers keyModifiers, Action setHandled) { if (key == Key.LeftAlt || key == Key.RightAlt || key == Key.LeftShift || key == Key.RightShift || key == Key.LeftCtrl - || key == Key.RightCtrl) return false; + || key == Key.RightCtrl) return; NoCommandFound = false; @@ -782,12 +831,14 @@ namespace FileTime.Avalonia.ViewModels if (key == Key.Escape) { - ShowAllShortcut = false; + IsAllShortcutVisible = false; _previousKeys.Clear(); PossibleCommands = new(); + setHandled(true); } else if (selectedCommandBinding != null) { + setHandled(true); await selectedCommandBinding.InvokeAsync(); _previousKeys.Clear(); PossibleCommands = new(); @@ -796,10 +847,11 @@ namespace FileTime.Avalonia.ViewModels { _previousKeys.Clear(); PossibleCommands = new(); - return false; + return; } else if (_previousKeys.Count == 2) { + setHandled(true); NoCommandFound = true; _previousKeys.Clear(); PossibleCommands = new(); @@ -817,6 +869,7 @@ namespace FileTime.Avalonia.ViewModels { PossibleCommands = possibleCommands; } + setHandled(true); } } else @@ -826,9 +879,10 @@ namespace FileTime.Avalonia.ViewModels if (key == Key.Escape) { - if (ShowAllShortcut) + setHandled(true); + if (IsAllShortcutVisible) { - ShowAllShortcut = false; + IsAllShortcutVisible = false; } else { @@ -839,12 +893,14 @@ namespace FileTime.Avalonia.ViewModels { if (AppState.RapidTravelText.Length > 0) { + setHandled(true); AppState.RapidTravelText = AppState.RapidTravelText.Substring(0, AppState.RapidTravelText.Length - 1); updateRapidTravelFilter = true; } } else if (keyString.Length == 1) { + setHandled(true); AppState.RapidTravelText += keyString.ToString().ToLower(); updateRapidTravelFilter = true; } @@ -854,12 +910,8 @@ namespace FileTime.Avalonia.ViewModels var selectedCommandBinding = _universalCommandBindings.Find(c => AreKeysEqual(c.Keys, currentKeyAsList)); if (selectedCommandBinding != null) { + setHandled(true); await selectedCommandBinding.InvokeAsync(); - return true; - } - else - { - return false; } } @@ -895,13 +947,6 @@ namespace FileTime.Avalonia.ViewModels } } } - - return true; - } - - public Task ProcessKeyUp(Key key, KeyModifiers keyModifiers) - { - return Task.FromResult(false); } private void ReadInputs(List inputs, Action inputHandler) @@ -1144,7 +1189,12 @@ namespace FileTime.Avalonia.ViewModels "show all shortcut", FileTime.App.Core.Command.Commands.Dummy, new KeyWithModifiers[] { new KeyWithModifiers(Key.F1) }, - ShowAllShortcut2), + ShowAllShortcut), + new CommandBinding( + "run command", + FileTime.App.Core.Command.Commands.Dummy, + new KeyWithModifiers[] { new KeyWithModifiers(Key.D4, shift: true) }, + RunCommandInContainer), //TODO REMOVE new CommandBinding( "open in default file browser", diff --git a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml index fc94da4..7dd5fa6 100644 --- a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml +++ b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml @@ -6,12 +6,12 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:FileTime.Avalonia.ViewModels" xmlns:local="using:FileTime.Avalonia.Views" + xmlns:models="using:FileTime.Avalonia.Models" Title="FileTime" d:DesignHeight="450" d:DesignWidth="800" Icon="/Assets/filetime.ico" InputElement.KeyDown="OnKeyDown" - InputElement.KeyUp="OnKeyUp" TransparencyLevelHint="Blur" Background="Transparent" ExtendClientAreaToDecorationsHint="True" @@ -100,7 +100,7 @@ Margin="5,0,0,0" Grid.Column="1" Grid.ColumnSpan="2" - Grid.Row="1" + Grid.Row="1" MinWidth="100" HorizontalAlignment="Stretch" Maximum="100" @@ -269,123 +269,65 @@ VerticalAlignment="Stretch" Fill="{DynamicResource ContentSeparatorBrush}" /> - - - - - - - - + + + + + + + + + - - - - Empty - - - + + FontWeight="Bold" + Foreground="{DynamicResource ErrorBrush}" + IsVisible="{Binding AppState.SelectedTab.ChildContainer.Exceptions.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}"> + Empty + - - - - - - - - - - - - - - - - - - - - - - - - - - + - + Margin="0,0,0,10" + HorizontalAlignment="Center" + TextWrapping="Wrap" + Text="There were some errors while opening container." + Foreground="{DynamicResource ErrorBrush}" /> + + + + + + + + - - - - -