Favorites

This commit is contained in:
2022-02-28 22:53:06 +01:00
parent c8748644d2
commit 899363b2b0
46 changed files with 790 additions and 41 deletions

View File

@@ -39,6 +39,7 @@ namespace FileTime.App.Core.Command
PasteMerge,
PasteOverwrite,
PasteSkip,
PinFavorite,
PreviousTimelineBlock,
PreviousTimelineCommand,
Refresh,

View File

@@ -4,6 +4,6 @@ namespace FileTime.App.Core.Models
{
public interface IHaveContainer
{
IContainer Container { get; }
IContainer? Container { get; }
}
}

View File

@@ -5,6 +5,7 @@ using FileTime.Core.ContainerSizeScanner;
using FileTime.Core.Providers;
using FileTime.Core.Services;
using FileTime.Core.Timeline;
using FileTime.Providers.Favorites;
using FileTime.Providers.Local;
using FileTime.Providers.Sftp;
using FileTime.Providers.Smb;
@@ -29,6 +30,7 @@ namespace FileTime.App.Core
.AddLocalServices()
.AddSmbServices()
.AddSftpServices()
.AddFavoriteServices()
.RegisterCommandHandlers();
}

View File

@@ -9,6 +9,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Providers\FileTime.Providers.Favorites\FileTime.Providers.Favorites.csproj" />
<ProjectReference Include="..\..\Providers\FileTime.Providers.Sftp\FileTime.Providers.Sftp.csproj" />
<ProjectReference Include="..\..\Tools\FileTime.Tools.Compression\FileTime.Tools.Compression.csproj" />
<ProjectReference Include="..\FileTime.App.Core\FileTime.App.Core.csproj" />

View File

@@ -337,6 +337,26 @@ namespace FileTime.Core.Components
await SetCurrentLocation(childContainer);
}
}
else if (_currentSelectedItem is ISymlinkElement symlinkElement)
{
if (symlinkElement.RealItem is IContainer realContainer)
{
await SetCurrentLocation(realContainer);
}
else if (symlinkElement.RealItem is IElement realElement)
{
if (realElement.GetParent() is IContainer parent)
{
await SetCurrentLocation(parent);
if (await _currentLocation.IsExistsAsync(realElement.Name))
{
var newRealElement = await _currentLocation.GetByPath(realElement.Name);
if (newRealElement != null) await SetCurrentSelectedItem(newRealElement);
}
}
}
}
}
public async Task OpenContainer(IContainer container) => await SetCurrentLocation(container);

View File

@@ -7,12 +7,10 @@ namespace FileTime.Core.ContainerSizeScanner
public class ContainerScanSnapshotProvider : ContentProviderBase<ContainerScanSnapshotProvider>
{
public ContainerScanSnapshotProvider() : base("size", null, "size://", false)
public ContainerScanSnapshotProvider() : base("size", "size://", false)
{
}
public override Task<bool> CanHandlePath(string path) => Task.FromResult(path.StartsWith(Protocol));
public override Task<IContainer> CreateContainerAsync(string name) => throw new NotSupportedException();
public override Task<IElement> CreateElementAsync(string name) => throw new NotSupportedException();

View File

@@ -32,7 +32,7 @@ namespace FileTime.Core.ContainerSizeScanner
public override Task<long?> GetElementSize(CancellationToken token = default) => Task.FromResult((long?)Size);
public override string GetPrimaryAttributeText() => "";
public override string? GetPrimaryAttributeText() => null;
public override Task Rename(string newName) => throw new NotSupportedException();
}

View File

@@ -0,0 +1,31 @@
using FileTime.Core.Providers;
namespace FileTime.Core.Models
{
public class AbsolutePathDto
{
public string? Path { get; set; }
public AbsolutePathType? Type { get; set; }
public string? ContentProviderName { get; set; }
public string? VirtualContentProviderName { get; set; }
public AbsolutePathDto() { }
public AbsolutePathDto(AbsolutePath path)
{
Path = path.Path;
Type = path.Type;
ContentProviderName = path.ContentProvider.Name;
VirtualContentProviderName = path.VirtualContentProvider?.Name;
}
public AbsolutePath Resolve(IEnumerable<IContentProvider> providers)
{
var contentProvider = providers.FirstOrDefault(p => p.Name == ContentProviderName) ?? throw new Exception($"Could not found content provider with name {ContentProviderName}");
var virtualContentProvider = VirtualContentProviderName != null ? providers.FirstOrDefault(p => p.Name == VirtualContentProviderName) : null;
if (Path is null) throw new Exception(nameof(Path) + " can not be null.");
if (Type is not AbsolutePathType type) throw new Exception(nameof(Type) + " can not be null.");
return new AbsolutePath(contentProvider, Path, type, virtualContentProvider);
}
}
}

View File

@@ -5,7 +5,7 @@ namespace FileTime.Core.Models
public interface IElement : IItem
{
bool IsSpecial { get; }
string GetPrimaryAttributeText();
string? GetPrimaryAttributeText();
Task<string> GetContent(CancellationToken token = default);
Task<long?> GetElementSize(CancellationToken token = default);

View File

@@ -0,0 +1,7 @@
namespace FileTime.Core.Models
{
public interface ISymlinkElement
{
IItem RealItem { get; }
}
}

View File

@@ -1,4 +1,3 @@
using System.Threading.Tasks;
using AsyncEvent;
using FileTime.Core.Models;

View File

@@ -51,7 +51,7 @@ namespace FileTime.Core.Providers
public abstract Task<long?> GetElementSize(CancellationToken token = default);
public IContainer? GetParent() => _parent;
public abstract string GetPrimaryAttributeText();
public abstract string? GetPrimaryAttributeText();
public abstract Task Rename(string newName);
}

View File

@@ -14,11 +14,15 @@ namespace FileTime.Core.Providers
protected List<IContainer>? RootContainers { get; private set; }
public override bool IsExists => true;
public virtual bool SupportsContentStreams { get; }
public virtual string Protocol { get; }
protected ContentProviderBase(
string name,
string? fullName,
string protocol,
bool supportsContentStreams)
bool supportsContentStreams,
string? fullName = null)
: base(name, fullName)
{
Protocol = protocol;
@@ -31,11 +35,7 @@ namespace FileTime.Core.Providers
CanDelete = SupportsDelete.False;
}
public virtual bool SupportsContentStreams { get; }
public virtual string Protocol { get; }
public abstract Task<bool> CanHandlePath(string path);
public virtual Task<bool> CanHandlePath(string path) => Task.FromResult(path.StartsWith(Protocol));
public override IContainer? GetParent() => _parent;
public void SetParent(IContainer parent) => _parent = parent;

View File

@@ -29,7 +29,7 @@ namespace FileTime.Core.Search
public override async Task<long?> GetElementSize(CancellationToken token = default) => await BaseElement.GetElementSize(token);
public override string GetPrimaryAttributeText() => BaseElement.GetPrimaryAttributeText();
public override string? GetPrimaryAttributeText() => BaseElement.GetPrimaryAttributeText();
public override Task Rename(string newName) => throw new NotSupportedException();
}

View File

@@ -42,7 +42,7 @@ namespace FileTime.Core.Timeline
public IContainer? GetParent() => _parent;
public string GetPrimaryAttributeText() => "";
public string? GetPrimaryAttributeText() => null;
public Task Rename(string newName) => Task.CompletedTask;

View File

@@ -7,7 +7,7 @@ namespace FileTime.Core.Timeline
{
private readonly PointInTime _pointInTime;
public TimeProvider(PointInTime pointInTime) : base("time", null, "time2://", false)
public TimeProvider(PointInTime pointInTime) : base("time", "time2://", false)
{
_pointInTime = pointInTime;
}

View File

@@ -43,6 +43,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.Providers.Sftp", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InitableService", "Core\InitableService\InitableService.csproj", "{B1520189-8646-4DE8-B5C9-46AE04B4D01C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.Providers.Favorites", "Providers\FileTime.Providers.Favorites\FileTime.Providers.Favorites.csproj", "{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -337,6 +339,26 @@ Global
{B1520189-8646-4DE8-B5C9-46AE04B4D01C}.Release|x64.Build.0 = Release|Any CPU
{B1520189-8646-4DE8-B5C9-46AE04B4D01C}.Release|x86.ActiveCfg = Release|Any CPU
{B1520189-8646-4DE8-B5C9-46AE04B4D01C}.Release|x86.Build.0 = Release|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Debug|ARM.ActiveCfg = Debug|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Debug|ARM.Build.0 = Debug|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Debug|ARM64.Build.0 = Debug|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Debug|x64.ActiveCfg = Debug|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Debug|x64.Build.0 = Debug|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Debug|x86.ActiveCfg = Debug|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Debug|x86.Build.0 = Debug|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Release|Any CPU.Build.0 = Release|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Release|ARM.ActiveCfg = Release|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Release|ARM.Build.0 = Release|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Release|ARM64.ActiveCfg = Release|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Release|ARM64.Build.0 = Release|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Release|x64.ActiveCfg = Release|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Release|x64.Build.0 = Release|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Release|x86.ActiveCfg = Release|Any CPU
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -356,6 +378,7 @@ Global
{B6F6A8F9-9B7B-4E3E-AE99-A90ECFDDC966} = {0D2B4BAA-0399-459C-B022-41DB7F408225}
{0E650206-801D-4E8D-95BA-4565B32092E1} = {517D96CE-A956-4638-A93D-465D34DE22B1}
{B1520189-8646-4DE8-B5C9-46AE04B4D01C} = {38B1B927-4201-4B7A-87EE-737B8C6D4090}
{27326C9B-FCB2-4A4F-9CCA-598C5244E26A} = {517D96CE-A956-4638-A93D-465D34DE22B1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8D679DCE-AC84-4A91-BFED-8F8D8E1D8183}

View File

@@ -11,6 +11,7 @@ using FileTime.Avalonia.Configuration;
using FileTime.Avalonia.Misc;
using FileTime.Core.Extensions;
using FileTime.Avalonia.ViewModels;
using FileTime.Providers.Favorites;
namespace FileTime.Avalonia.Application
{
@@ -48,6 +49,9 @@ namespace FileTime.Avalonia.Application
[Property]
private bool _noCommandFound;
[Property]
private List<IItem> _favoriteElements;
public List<KeyConfig> PreviousKeys { get; } = new();
public ObservableCollection<ParallelCommandsViewModel> TimelineCommands { get; } = new();

View File

@@ -58,7 +58,7 @@ namespace FileTime.Avalonia.Configuration
new CommandBindingConfiguration(Commands.FindByName, new[] { Key.F, Key.N }),
new CommandBindingConfiguration(Commands.FindByNameRegex, new[] { Key.F, Key.R }),
new CommandBindingConfiguration(Commands.GoToHome, new[] { Key.G, Key.H }),
new CommandBindingConfiguration(Commands.GoToPath, new KeyConfig(Key.OemComma, ctrl: true)),
new CommandBindingConfiguration(Commands.GoToPath, new KeyConfig(Key.L, ctrl: true)),
new CommandBindingConfiguration(Commands.GoToPath, new[] { Key.G, Key.P }),
new CommandBindingConfiguration(Commands.GoToProvider, new[] { Key.G, Key.T }),
new CommandBindingConfiguration(Commands.GoToRoot, new[] { Key.G, Key.R }),
@@ -72,6 +72,7 @@ namespace FileTime.Avalonia.Configuration
new CommandBindingConfiguration(Commands.PasteMerge, new[] { Key.P, Key.P }),
new CommandBindingConfiguration(Commands.PasteOverwrite, new[] { Key.P, Key.O }),
new CommandBindingConfiguration(Commands.PasteSkip, new[] { Key.P, Key.S }),
new CommandBindingConfiguration(Commands.PinFavorite, new[] { Key.F, Key.P }),
new CommandBindingConfiguration(Commands.PreviousTimelineBlock, Key.H ),
new CommandBindingConfiguration(Commands.PreviousTimelineCommand, Key.K ),
new CommandBindingConfiguration(Commands.Refresh, Key.R),

View File

@@ -53,6 +53,7 @@
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core\FileTime.App.Core.csproj" />
<ProjectReference Include="..\..\AppCommon\FileTime.App.DependencyInjection\FileTime.App.DependencyInjection.csproj" />
<ProjectReference Include="..\..\Core\InitableService\InitableService.csproj" />
<ProjectReference Include="..\..\Providers\FileTime.Providers.Favorites\FileTime.Providers.Favorites.csproj" />
</ItemGroup>
<ItemGroup>
<AvaloniaXaml Update="Views\MainWindow.axaml">

View File

@@ -10,7 +10,6 @@ namespace FileTime.Avalonia.IconProviders
{
public class MaterialIconProvider : IIconProvider
{
private static readonly Dictionary<string, string> _iconsByExtension = new();
private static readonly Dictionary<string, string> _iconsByFileName = new();
@@ -38,6 +37,7 @@ namespace FileTime.Avalonia.IconProviders
public ImagePath GetImage(IItem item)
{
item = item is ISymlinkElement symlinkElement ? symlinkElement.RealItem : item;
var icon = item is IContainer ? "folder.svg" : "file.svg";
string? localPath = item switch
{

View File

@@ -6,7 +6,7 @@ namespace FileTime.Avalonia.Models
public class PlaceInfo : IHaveContainer
{
public string Name { get; }
public IContainer Container { get; }
public IContainer? Container { get; }
public PlaceInfo(string name, IContainer container)
{

View File

@@ -13,7 +13,7 @@ namespace FileTime.Avalonia.Models
private readonly DriveInfo _driveInfo;
private readonly IContainer _container;
public IContainer Container => _container;
public IContainer? Container => _container;
[Property]
private string _name;

View File

@@ -26,6 +26,7 @@ using FileTime.Core.Providers;
using FileTime.Core.Search;
using FileTime.Core.Services;
using FileTime.Core.Timeline;
using FileTime.Providers.Favorites;
using FileTime.Providers.Local;
using FileTime.Tools.Compression.Command;
using Microsoft.Extensions.Logging;
@@ -49,6 +50,7 @@ namespace FileTime.Avalonia.Services
private readonly ILogger<CommandHandlerService> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly ContainerScanSnapshotProvider _containerScanSnapshotProvider;
private readonly FavoriteContentProvider _favoriteContentProvider;
public CommandHandlerService(
AppState appState,
@@ -62,7 +64,8 @@ namespace FileTime.Avalonia.Services
ProgramsService programsService,
ILogger<CommandHandlerService> logger,
IServiceProvider serviceProvider,
ContainerScanSnapshotProvider containerScanSnapshotProvider)
ContainerScanSnapshotProvider containerScanSnapshotProvider,
FavoriteContentProvider favoriteContentProvider)
{
_appState = appState;
_localContentProvider = localContentProvider;
@@ -76,6 +79,7 @@ namespace FileTime.Avalonia.Services
_logger = logger;
_serviceProvider = serviceProvider;
_containerScanSnapshotProvider = containerScanSnapshotProvider;
_favoriteContentProvider = favoriteContentProvider;
_commandHandlers = new Dictionary<Commands, Func<Task>>
{
@@ -114,6 +118,7 @@ namespace FileTime.Avalonia.Services
{Commands.PasteMerge, PasteMerge},
{Commands.PasteOverwrite, PasteOverwrite},
{Commands.PasteSkip, PasteSkip},
{Commands.PinFavorite, PinFavorite},
{Commands.PreviousTimelineBlock, SelectPreviousTimelineBlock},
{Commands.PreviousTimelineCommand, SelectPreviousTimelineCommand},
{Commands.Refresh, RefreshCurrentLocation},
@@ -1011,5 +1016,30 @@ namespace FileTime.Avalonia.Services
await OpenContainer(scanTask.Snapshot);
}
}
private async Task PinFavorite()
{
if (_appState.SelectedTab.SelectedItem is IItemViewModel selectedItemVM)
{
if (selectedItemVM.BaseItem is FavoriteElement favoriteElement)
{
favoriteElement.IsPinned = !favoriteElement.IsPinned;
_appState.FavoriteElements = GetFavoriteElements(_favoriteContentProvider).Select(f => f.BaseItem).ToList();
await _favoriteContentProvider.SaveAsync();
}
else
{
_dialogService.ShowToastMessage("Selected item is not a favorite element.");
}
}
static IEnumerable<FavoriteElement> GetFavoriteElements(FavoriteContainerBase container)
{
return container.Elements.Where(e => e.IsPinned).Concat(
container.Containers.SelectMany(GetFavoriteElements)
);
}
}
}
}

View File

@@ -1,11 +1,12 @@
using FileTime.Core.Models;
using FileTime.App.Core.Models;
using FileTime.Core.Models;
using InitableService;
using System;
using System.Threading.Tasks;
namespace FileTime.Avalonia.ViewModels
{
public class HistoryItemViewModel : IAsyncInitable<AbsolutePath>
public class HistoryItemViewModel : IAsyncInitable<AbsolutePath>, IHaveContainer
{
public string? Name { get; private set; }
public IContainer? Container { get; private set; }

View File

@@ -22,6 +22,7 @@ using System.Threading;
using Avalonia.Input;
using System.Reflection;
using FileTime.Core.Services;
using FileTime.Providers.Favorites;
namespace FileTime.Avalonia.ViewModels
{
@@ -72,6 +73,7 @@ namespace FileTime.Avalonia.ViewModels
}
Title = "FileTime " + versionString;
var favoriteContentProvider = App.ServiceProvider.GetRequiredService<FavoriteContentProvider>();
_timeRunner = App.ServiceProvider.GetService<TimeRunner>()!;
var inputInterface = (BasicInputHandler)App.ServiceProvider.GetService<IInputInterface>()!;
inputInterface.InputHandler = _dialogService.ReadInputs;
@@ -174,9 +176,22 @@ namespace FileTime.Avalonia.ViewModels
throw new Exception("TODO linux places");
}
Places = places;
await favoriteContentProvider.InitIfNeeded();
AppState.FavoriteElements = GetFavoriteElements(favoriteContentProvider).Select(f => f.BaseItem).ToList();
await Task.Delay(100);
Loading = false;
_logger?.LogInformation($"{nameof(MainPageViewModel)} initialized.");
static IEnumerable<FavoriteElement> GetFavoriteElements(FavoriteContainerBase container)
{
return container.Elements.Where(e => e.IsPinned).Concat(
container.Containers.SelectMany(GetFavoriteElements)
);
}
}
private Task UpdateParallelCommands(object? sender, IReadOnlyList<ReadOnlyParallelCommands> parallelCommands, CancellationToken token)
@@ -269,7 +284,9 @@ namespace FileTime.Avalonia.ViewModels
public void ProcessKeyDown(Key key, KeyModifiers keyModifiers, Action<bool> setHandled)
{
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
KeyInputHandlerService.ProcessKeyDown(key, keyModifiers, setHandled);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}
}
}

View File

@@ -41,7 +41,7 @@
</StackPanel>
</Grid>
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,Auto">
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,Auto,Auto">
<Border CornerRadius="10" Background="{DynamicResource ContainerBackgroundBrush}" Padding="10" Margin="10">
<Grid RowDefinitions="Auto,Auto">
@@ -149,6 +149,38 @@
</Border>
<Border Grid.Row="2" CornerRadius="10" Background="{DynamicResource ContainerBackgroundBrush}" Padding="0,10" Margin="10">
<Grid RowDefinitions="Auto,Auto">
<TextBlock
Margin="10,0,10,10"
Text="Favorites" />
<ItemsRepeater
Grid.Row="1"
Items="{Binding AppState.FavoriteElements}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Grid Classes="SidebarContainerPresenter" PointerPressed="OnHasContainerPointerPressed" Cursor="Hand">
<StackPanel Orientation="Horizontal" Margin="10,5" HorizontalAlignment="Stretch">
<Image
Width="20"
Height="20"
VerticalAlignment="Center"
Source="{Binding Converter={StaticResource ItemToImageConverter}}" />
<TextBlock
Margin="5,0,0,0"
VerticalAlignment="Center"
Text="{Binding Name}" />
</StackPanel>
</Grid>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Grid>
</Border>
<Border Grid.Row="3" CornerRadius="10" Background="{DynamicResource ContainerBackgroundBrush}" Padding="0,10" Margin="10">
<Grid RowDefinitions="Auto,Auto">
<TextBlock

View File

@@ -3,14 +3,17 @@ using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using FileTime.App.Core.Models;
using FileTime.Avalonia.Misc;
using FileTime.Avalonia.Models;
using FileTime.Avalonia.ViewModels;
using FileTime.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace FileTime.Avalonia.Views
{
@@ -94,18 +97,38 @@ namespace FileTime.Avalonia.Views
}
}
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
private void OnHasContainerPointerPressed(object sender, PointerPressedEventArgs e)
{
if (!e.Handled
&& ViewModel != null
&& e.GetCurrentPoint(this).Properties.IsLeftButtonPressed
&& sender is StyledElement control
&& control.DataContext is IHaveContainer hasContainer)
&& sender is StyledElement control)
{
if (control.DataContext is IHaveContainer hasContainer
&& hasContainer.Container is not null)
{
ViewModel.CommandHandlerService.OpenContainer(hasContainer.Container);
e.Handled = true;
}
else if (control.DataContext is IContainer container)
{
ViewModel.CommandHandlerService.OpenContainer(container);
}
else if (control.DataContext is IElement element && element.GetParent() is IContainer parentContainer)
{
Task.Run(async () =>
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
await ViewModel.AppState.SelectedTab.OpenContainer(parentContainer);
await ViewModel.AppState.SelectedTab.SetCurrentSelectedItem(element);
});
});
}
}
}
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
private void OnWindowClosed(object sender, EventArgs e)
{

View File

@@ -0,0 +1,33 @@
using FileTime.Core.Command;
using FileTime.Core.Command.Copy;
using FileTime.Core.Timeline;
namespace FileTime.Providers.Favorites.CommandHandlers
{
public class ToFavoriteCopyCommandHandler : ICommandHandler
{
public bool CanHandle(object command)
{
return command is CopyCommand copyCommand
&& copyCommand.Target?.ContentProvider is FavoriteContentProvider;
}
public async Task ExecuteAsync(object command, TimeRunner timeRunner)
{
if (command is not CopyCommand copyCommand) throw new ArgumentException($"Command must be {typeof(CopyCommand)}.", nameof(command));
if (copyCommand.Target is null) throw new NullReferenceException("Command's target can not be null.");
var resolvedTarget = await copyCommand.Target.ResolveAsync();
if (resolvedTarget is not FavoriteContainerBase targetContainer) throw new Exception($"Target is not {nameof(FavoriteContainerBase)}.");
foreach (var source in copyCommand.Sources)
{
var resolvedSource = await source.ResolveAsync();
if (resolvedSource == null) continue;
var newElement = new FavoriteElement(targetContainer, resolvedSource.Name, resolvedSource);
await targetContainer.AddElementAsync(newElement);
}
}
}
}

View File

@@ -0,0 +1,12 @@
using FileTime.Core.Models;
using FileTime.Providers.Favorites.Persistence;
namespace FileTime.Providers.Favorites
{
public class FavoriteContainer : FavoriteContainerBase, IFavoriteItem
{
public FavoriteContainer(PersistenceService persistenceService, FavoriteContentProvider provider, IContainer parent, string name) : base(persistenceService, provider, parent, name)
{
}
}
}

View File

@@ -0,0 +1,211 @@
using AsyncEvent;
using FileTime.Core.Models;
using FileTime.Core.Providers;
using FileTime.Providers.Favorites.Persistence;
namespace FileTime.Providers.Favorites
{
public abstract class FavoriteContainerBase : IContainer
{
private readonly List<FavoriteContainer> _containers;
private List<IItem> _items;
private readonly List<FavoriteElement> _elements;
private readonly List<Exception> _exceptions = new();
private readonly PersistenceService _persistenceService;
public bool IsExists => true;
public IReadOnlyList<Exception> Exceptions { get; }
public bool AllowRecursiveDeletion => false;
public bool Loading => false;
public bool CanHandleEscape => false;
public bool IsLoaded => true;
public bool SupportsDirectoryLevelSoftDelete => false;
public AsyncEventHandler Refreshed { get; } = new();
public AsyncEventHandler<bool> LoadingChanged { get; } = new();
public string Name { get; }
public string DisplayName { get; }
public string? FullName { get; }
public string? NativePath { get; }
public bool IsHidden => false;
public bool IsDestroyed => false;
public SupportsDelete CanDelete => SupportsDelete.True;
public bool CanRename => true;
public FavoriteContentProvider Provider { get; }
IContentProvider IItem.Provider => Provider;
protected IContainer? Parent { get; set; }
public IReadOnlyList<FavoriteContainer> Containers { get; }
public IReadOnlyList<FavoriteElement> Elements { get; }
protected FavoriteContainerBase(PersistenceService persistenceService, FavoriteContentProvider provider, IContainer parent, string name)
: this(persistenceService, name, parent.FullName == null ? name : parent.FullName + Constants.SeparatorChar + name)
{
Provider = provider;
Parent = parent;
}
protected FavoriteContainerBase(PersistenceService persistenceService, string name)
: this(persistenceService, name, null)
{
Provider = (FavoriteContentProvider)this;
}
private FavoriteContainerBase(PersistenceService persistenceService, string name, string? fullName)
{
_containers = new List<FavoriteContainer>();
_items = new List<IItem>();
_elements = new List<FavoriteElement>();
_persistenceService = persistenceService;
Containers = _containers.AsReadOnly();
Elements = _elements.AsReadOnly();
Exceptions = _exceptions.AsReadOnly();
DisplayName = Name = name;
NativePath = FullName = fullName;
Provider = null!;
}
public Task<IContainer> CloneAsync() => Task.FromResult((IContainer)this);
public async Task<IContainer> CreateContainerAsync(string name)
{
var container = new FavoriteContainer(_persistenceService, Provider, this, name);
await AddContainerAsync(container);
return container;
}
public Task<IElement> CreateElementAsync(string name) => throw new NotSupportedException();
public Task Delete(bool hardDelete = false)
{
throw new NotImplementedException();
}
public async Task RefreshAsync(CancellationToken token = default)
{
if (Refreshed != null) await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty, token);
}
public Task Rename(string newName)
{
throw new NotImplementedException();
}
protected async Task SaveFavoritesAsync()
{
await _persistenceService.SaveFavorites(Provider.Containers.Cast<IFavoriteItem>().Concat(Provider.Elements.Cast<IFavoriteItem>()));
}
public async Task AddContainerAsync(FavoriteContainer container)
{
_containers.Add(container);
UpdateItems();
await SaveFavoritesAsync();
await RefreshAsync();
}
public async Task AddContainersAsync(IEnumerable<FavoriteContainer> containers)
{
_containers.AddRange(containers);
UpdateItems();
await SaveFavoritesAsync();
await RefreshAsync();
}
public async Task DeleteContainerAsync(FavoriteContainer container)
{
_containers.Remove(container);
UpdateItems();
await RefreshAsync();
}
public async Task AddElementAsync(FavoriteElement element)
{
_elements.Add(element);
UpdateItems();
await SaveFavoritesAsync();
await RefreshAsync();
}
public async Task AddElementsAsync(IEnumerable<FavoriteElement> elements)
{
_elements.AddRange(elements);
UpdateItems();
await SaveFavoritesAsync();
await RefreshAsync();
}
public async Task DeleteElementAsync(FavoriteElement element)
{
_elements.Remove(element);
UpdateItems();
await RefreshAsync();
}
private void UpdateItems()
{
_items = _containers.Cast<IItem>().Concat(_elements).ToList();
}
public async Task<IReadOnlyList<IContainer>?> GetContainers(CancellationToken token = default)
{
await InitIfNeeded();
return _containers;
}
public async Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default)
{
await InitIfNeeded();
return _elements;
}
public async Task<IReadOnlyList<IItem>?> GetItems(CancellationToken token = default)
{
await InitIfNeeded();
return _items;
}
public virtual Task InitIfNeeded() => Task.CompletedTask;
public async Task<bool> IsExistsAsync(string name)
{
var items = await GetItems();
return items?.Any(i => i.Name == name) ?? false;
}
public virtual Task<bool> CanOpenAsync() => Task.FromResult(_exceptions.Count == 0);
public void Unload() { }
public Task<ContainerEscapeResult> HandleEscape() => throw new NotSupportedException();
public async Task RunWithLoading(Func<CancellationToken, Task> func, CancellationToken token = default)
{
await func(token);
}
public void Destroy() { }
public IContainer? GetParent() => Parent;
}
}

View File

@@ -0,0 +1,78 @@
using FileTime.Core.Models;
using FileTime.Core.Providers;
using FileTime.Providers.Favorites.Persistence;
using Microsoft.Extensions.DependencyInjection;
namespace FileTime.Providers.Favorites
{
public class FavoriteContentProvider : FavoriteContainerBase, IContentProvider
{
private bool _initialized;
private readonly PersistenceService _persistenceService;
private readonly Lazy<IEnumerable<IContentProvider>> _contentProvidersLazy;
public FavoriteContentProvider(PersistenceService persistenceService, IServiceProvider serviceProvider) : base(persistenceService, "favorite")
{
Protocol = "favorite://";
_persistenceService = persistenceService;
_contentProvidersLazy = new Lazy<IEnumerable<IContentProvider>>(() => serviceProvider.GetRequiredService<IEnumerable<IContentProvider>>());
}
public bool SupportsContentStreams => false;
public string Protocol { get; }
public Task<bool> CanHandlePath(string path) => Task.FromResult(path.StartsWith(Protocol));
public void SetParent(IContainer container)
{
Parent = container;
}
public async Task SaveAsync()
{
await SaveFavoritesAsync();
}
public override async Task InitIfNeeded()
{
if (!_initialized)
{
_initialized = true;
var (containerDtos, elementDtos) = await _persistenceService.LoadFavorites();
await AddItems(this, containerDtos, elementDtos);
}
}
private async Task AddItems(
FavoriteContainerBase container,
IEnumerable<FavoriteContainerDto> containerDtos,
IEnumerable<FavoriteElementDto> elementDtos)
{
var newContainers = new List<FavoriteContainer>();
var newElements = new List<FavoriteElement>();
foreach (var containerDto in containerDtos)
{
var newContainer = new FavoriteContainer(_persistenceService, this, container, containerDto.Name);
newContainers.Add(newContainer);
await AddItems(newContainer, containerDto.Containers, containerDto.Elements);
}
foreach (var elementDto in elementDtos)
{
var item = await elementDto.RealPath.Resolve(_contentProvidersLazy.Value).ResolveAsync();
if (item is not null)
{
var newElement = new FavoriteElement(container, elementDto.Name, item, elementDto.IsPinned);
newElements.Add(newElement);
}
}
await container.AddContainersAsync(newContainers);
await container.AddElementsAsync(newElements);
}
}
}

View File

@@ -0,0 +1,33 @@
using FileTime.Core.Models;
using FileTime.Core.Providers;
namespace FileTime.Providers.Favorites
{
public class FavoriteElement : AbstractElement<FavoriteContentProvider>, ISymlinkElement, IFavoriteItem
{
public IItem BaseItem { get; }
public bool IsPinned { get; set; }
IItem ISymlinkElement.RealItem => BaseItem;
public FavoriteElement(FavoriteContainerBase parent, string name, IItem baseItem, bool isPinned = false) : base(parent.Provider, parent, name)
{
BaseItem = baseItem;
IsPinned = isPinned;
}
public override Task Delete(bool hardDelete = false) => throw new NotSupportedException();
public override Task<string> GetContent(CancellationToken token = default) => throw new NotSupportedException();
public override Task<IContentReader> GetContentReaderAsync() => throw new NotSupportedException();
public override Task<IContentWriter> GetContentWriterAsync() => throw new NotSupportedException();
public override Task<long?> GetElementSize(CancellationToken token = default) => Task.FromResult((long?)null);
public override string? GetPrimaryAttributeText() => null;
public override Task Rename(string newName) => throw new NotSupportedException();
}
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core\FileTime.App.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,4 @@
namespace FileTime.Providers.Favorites
{
public interface IFavoriteItem { }
}

View File

@@ -0,0 +1,42 @@
namespace FileTime.Providers.Favorites.Persistence
{
public class FavoriteContainerDto
{
public List<FavoriteContainerDto> Containers { get; set; } = new();
public List<FavoriteElementDto> Elements { get; set; } = new();
public string Name { get; set; } = null!;
public async Task Init(FavoriteContainer favoriteContainer)
{
Name = favoriteContainer.Name;
var newContainers = new List<FavoriteContainerDto>();
var newElements = new List<FavoriteElementDto>();
var containers = await favoriteContainer.GetContainers();
var elements = await favoriteContainer.GetElements();
if (containers != null)
{
foreach (var container in containers.Cast<FavoriteContainer>())
{
var childFavorite = new FavoriteContainerDto();
await childFavorite.Init(container);
newContainers.Add(childFavorite);
}
}
if (elements != null)
{
foreach (var element in elements.Cast<FavoriteElement>())
{
var childFavorite = new FavoriteElementDto(element);
newElements.Add(childFavorite);
}
}
Containers = newContainers;
Elements = newElements;
}
}
}

View File

@@ -0,0 +1,20 @@
using FileTime.Core.Models;
namespace FileTime.Providers.Favorites.Persistence
{
public class FavoriteElementDto
{
public string Name { get; set; } = null!;
public AbsolutePathDto RealPath { get; set; } = null!;
public bool IsPinned { get; set; }
public FavoriteElementDto() { }
public FavoriteElementDto(FavoriteElement element)
{
Name = element.Name;
RealPath = new AbsolutePathDto(new AbsolutePath(element.BaseItem));
IsPinned = element.IsPinned;
}
}
}

View File

@@ -0,0 +1,8 @@
namespace FileTime.Providers.Favorites.Persistence
{
public class FavoritePersistenceRoot
{
public List<FavoriteContainerDto> Containers { get; set; } = new();
public List<FavoriteElementDto> Elements { get; set; } = new();
}
}

View File

@@ -0,0 +1,71 @@
using System.Text.Json;
using FileTime.Core.Persistence;
namespace FileTime.Providers.Favorites.Persistence
{
public class PersistenceService
{
private const string favoriteFolderName = "favorites";
private const string favoriteFileName = "favorites.json";
private readonly PersistenceSettings _persistenceSettings;
private readonly JsonSerializerOptions _jsonOptions;
public PersistenceService(PersistenceSettings persistenceSettings)
{
_persistenceSettings = persistenceSettings;
_jsonOptions = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true,
WriteIndented = true
};
}
public async Task SaveFavorites(IEnumerable<IFavoriteItem> items)
{
var containers = new List<FavoriteContainerDto>();
var elements = new List<FavoriteElementDto>();
foreach (var favoriteItem in items)
{
if (favoriteItem is FavoriteContainer favoriteContainer)
{
var childFavorite = new FavoriteContainerDto();
await childFavorite.Init(favoriteContainer);
containers.Add(childFavorite);
}
else if (favoriteItem is FavoriteElement favoriteElement)
{
var childFavorite = new FavoriteElementDto(favoriteElement);
elements.Add(childFavorite);
}
}
var root = new FavoritePersistenceRoot()
{
Containers = containers,
Elements = elements
};
var favoriteDirectory = new DirectoryInfo(Path.Combine(_persistenceSettings.RootAppDataPath, favoriteFolderName));
if (!favoriteDirectory.Exists) favoriteDirectory.Create();
var persistencePath = Path.Combine(_persistenceSettings.RootAppDataPath, favoriteFolderName, favoriteFileName);
using var stream = File.Create(persistencePath);
await JsonSerializer.SerializeAsync(stream, root, _jsonOptions);
}
public async Task<(IEnumerable<FavoriteContainerDto>, IEnumerable<FavoriteElementDto>)> LoadFavorites()
{
var persistencePath = Path.Combine(_persistenceSettings.RootAppDataPath, favoriteFolderName, favoriteFileName);
if (!new FileInfo(persistencePath).Exists) return (Enumerable.Empty<FavoriteContainerDto>(), Enumerable.Empty<FavoriteElementDto>());
using var stream = File.OpenRead(persistencePath);
var serversRoot = (await JsonSerializer.DeserializeAsync<FavoritePersistenceRoot>(stream))!;
return (serversRoot.Containers, serversRoot.Elements);
}
}
}

View File

@@ -0,0 +1,35 @@
using FileTime.Core.Command;
using FileTime.Core.Providers;
using FileTime.Providers.Favorites.CommandHandlers;
using FileTime.Providers.Favorites.Persistence;
using Microsoft.Extensions.DependencyInjection;
namespace FileTime.Providers.Favorites
{
public static class Startup
{
public static IServiceCollection AddFavoriteServices(this IServiceCollection serviceCollection)
{
return serviceCollection
.AddSingleton<FavoriteContentProvider>()
.AddSingleton<IContentProvider>(serviceProvider => serviceProvider.GetRequiredService<FavoriteContentProvider>())
.AddSingleton<PersistenceService>()
.RegisterFavoriteCommandHandlers();
}
internal static IServiceCollection RegisterFavoriteCommandHandlers(this IServiceCollection serviceCollection)
{
var commandHandlers = new List<Type>()
{
typeof(ToFavoriteCopyCommandHandler)
};
foreach (var commandHandler in commandHandlers)
{
serviceCollection.AddTransient(typeof(ICommandHandler), commandHandler);
}
return serviceCollection;
}
}
}

View File

@@ -11,7 +11,7 @@ namespace FileTime.Providers.Local
public bool IsCaseInsensitive { get; }
public LocalContentProvider(ILogger<LocalContentProvider> logger)
: base("local", null, "local://", true)
: base("local", "local://", true)
{
_logger = logger;

View File

@@ -45,7 +45,7 @@ namespace FileTime.Providers.Local
Provider = contentProvider;
}
public string GetPrimaryAttributeText() => File.Length.ToSizeString();
public string? GetPrimaryAttributeText() => File.Length.ToSizeString();
public Task Delete(bool hardDelete = false)
{

View File

@@ -11,14 +11,12 @@ namespace FileTime.Providers.Sftp
private readonly ILogger<SftpContentProvider> _logger;
public SftpContentProvider(IInputInterface inputInterface, ILogger<SftpContentProvider> logger)
: base("sftp", null, "sftp://", false)
: base("sftp", "sftp://", false)
{
_logger = logger;
_inputInterface = inputInterface;
}
public override Task<bool> CanHandlePath(string path) => Task.FromResult(path.StartsWith(Protocol));
public override async Task<IContainer> CreateContainerAsync(string name)
{
var container = RootContainers?.FirstOrDefault(c => c.Name == name);

View File

@@ -61,7 +61,7 @@ namespace FileTime.Providers.Sftp
throw new NotImplementedException();
}
public string GetPrimaryAttributeText()
public string? GetPrimaryAttributeText()
{
throw new NotImplementedException();
}

View File

@@ -13,7 +13,7 @@ namespace FileTime.Providers.Smb
private readonly ILogger<SmbContentProvider> _logger;
public SmbContentProvider(IInputInterface inputInterface, Persistence.PersistenceService persistenceService, ILogger<SmbContentProvider> logger)
: base("smb", null, "smb://", true)
: base("smb", "smb://", true)
{
_inputInterface = inputInterface;
_persistenceService = persistenceService;

View File

@@ -74,10 +74,7 @@ namespace FileTime.Providers.Smb
throw new NotImplementedException();
}
public string GetPrimaryAttributeText()
{
return "";
}
public string? GetPrimaryAttributeText() => null;
public IContainer? GetParent() => _parent;
public Task<string> GetContent(CancellationToken token = default) => Task.FromResult("NotImplemented");