diff --git a/src/AppCommon/FileTime.App.Core/Command/Commands.cs b/src/AppCommon/FileTime.App.Core/Command/Commands.cs
index 9de49cc..22a7765 100644
--- a/src/AppCommon/FileTime.App.Core/Command/Commands.cs
+++ b/src/AppCommon/FileTime.App.Core/Command/Commands.cs
@@ -39,6 +39,7 @@ namespace FileTime.App.Core.Command
PasteMerge,
PasteOverwrite,
PasteSkip,
+ PinFavorite,
PreviousTimelineBlock,
PreviousTimelineCommand,
Refresh,
diff --git a/src/AppCommon/FileTime.App.Core/Models/IHaveContainer.cs b/src/AppCommon/FileTime.App.Core/Models/IHaveContainer.cs
index b658577..7f6d0d9 100644
--- a/src/AppCommon/FileTime.App.Core/Models/IHaveContainer.cs
+++ b/src/AppCommon/FileTime.App.Core/Models/IHaveContainer.cs
@@ -4,6 +4,6 @@ namespace FileTime.App.Core.Models
{
public interface IHaveContainer
{
- IContainer Container { get; }
+ IContainer? Container { get; }
}
}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs
index ad17d44..823903a 100644
--- a/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs
+++ b/src/AppCommon/FileTime.App.DependencyInjection/DependencyInjection.cs
@@ -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();
}
diff --git a/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj b/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj
index f3a2791..854748f 100644
--- a/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj
+++ b/src/AppCommon/FileTime.App.DependencyInjection/FileTime.App.DependencyInjection.csproj
@@ -9,6 +9,7 @@
+
diff --git a/src/Core/FileTime.Core/Components/Tab.cs b/src/Core/FileTime.Core/Components/Tab.cs
index 6cdaffc..477a1d6 100644
--- a/src/Core/FileTime.Core/Components/Tab.cs
+++ b/src/Core/FileTime.Core/Components/Tab.cs
@@ -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);
diff --git a/src/Core/FileTime.Core/ContainerSizeScanner/ContainerScanSnapshotProvider.cs b/src/Core/FileTime.Core/ContainerSizeScanner/ContainerScanSnapshotProvider.cs
index ff8a91e..948b158 100644
--- a/src/Core/FileTime.Core/ContainerSizeScanner/ContainerScanSnapshotProvider.cs
+++ b/src/Core/FileTime.Core/ContainerSizeScanner/ContainerScanSnapshotProvider.cs
@@ -7,12 +7,10 @@ namespace FileTime.Core.ContainerSizeScanner
public class ContainerScanSnapshotProvider : ContentProviderBase
{
- public ContainerScanSnapshotProvider() : base("size", null, "size://", false)
+ public ContainerScanSnapshotProvider() : base("size", "size://", false)
{
}
- public override Task CanHandlePath(string path) => Task.FromResult(path.StartsWith(Protocol));
-
public override Task CreateContainerAsync(string name) => throw new NotSupportedException();
public override Task CreateElementAsync(string name) => throw new NotSupportedException();
diff --git a/src/Core/FileTime.Core/ContainerSizeScanner/ContainerSizeElement.cs b/src/Core/FileTime.Core/ContainerSizeScanner/ContainerSizeElement.cs
index af04fad..eb94ad0 100644
--- a/src/Core/FileTime.Core/ContainerSizeScanner/ContainerSizeElement.cs
+++ b/src/Core/FileTime.Core/ContainerSizeScanner/ContainerSizeElement.cs
@@ -32,7 +32,7 @@ namespace FileTime.Core.ContainerSizeScanner
public override Task 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();
}
diff --git a/src/Core/FileTime.Core/Models/AbsolutePathDto.cs b/src/Core/FileTime.Core/Models/AbsolutePathDto.cs
new file mode 100644
index 0000000..477594a
--- /dev/null
+++ b/src/Core/FileTime.Core/Models/AbsolutePathDto.cs
@@ -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 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Core/FileTime.Core/Models/IElement.cs b/src/Core/FileTime.Core/Models/IElement.cs
index 1a68ddb..2b4596e 100644
--- a/src/Core/FileTime.Core/Models/IElement.cs
+++ b/src/Core/FileTime.Core/Models/IElement.cs
@@ -5,7 +5,7 @@ namespace FileTime.Core.Models
public interface IElement : IItem
{
bool IsSpecial { get; }
- string GetPrimaryAttributeText();
+ string? GetPrimaryAttributeText();
Task GetContent(CancellationToken token = default);
Task GetElementSize(CancellationToken token = default);
diff --git a/src/Core/FileTime.Core/Models/ISymlinkElement.cs b/src/Core/FileTime.Core/Models/ISymlinkElement.cs
new file mode 100644
index 0000000..35080e0
--- /dev/null
+++ b/src/Core/FileTime.Core/Models/ISymlinkElement.cs
@@ -0,0 +1,7 @@
+namespace FileTime.Core.Models
+{
+ public interface ISymlinkElement
+ {
+ IItem RealItem { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Core/FileTime.Core/Providers/AbstractContainer.cs b/src/Core/FileTime.Core/Providers/AbstractContainer.cs
index 55c5093..ac84456 100644
--- a/src/Core/FileTime.Core/Providers/AbstractContainer.cs
+++ b/src/Core/FileTime.Core/Providers/AbstractContainer.cs
@@ -1,4 +1,3 @@
-using System.Threading.Tasks;
using AsyncEvent;
using FileTime.Core.Models;
diff --git a/src/Core/FileTime.Core/Providers/AbstractElement.cs b/src/Core/FileTime.Core/Providers/AbstractElement.cs
index 891bee3..acb9ea5 100644
--- a/src/Core/FileTime.Core/Providers/AbstractElement.cs
+++ b/src/Core/FileTime.Core/Providers/AbstractElement.cs
@@ -51,7 +51,7 @@ namespace FileTime.Core.Providers
public abstract Task GetElementSize(CancellationToken token = default);
public IContainer? GetParent() => _parent;
- public abstract string GetPrimaryAttributeText();
+ public abstract string? GetPrimaryAttributeText();
public abstract Task Rename(string newName);
}
diff --git a/src/Core/FileTime.Core/Providers/ContentProviderBase.cs b/src/Core/FileTime.Core/Providers/ContentProviderBase.cs
index 439bce5..9fd9dec 100644
--- a/src/Core/FileTime.Core/Providers/ContentProviderBase.cs
+++ b/src/Core/FileTime.Core/Providers/ContentProviderBase.cs
@@ -14,11 +14,15 @@ namespace FileTime.Core.Providers
protected List? 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 CanHandlePath(string path);
+ public virtual Task CanHandlePath(string path) => Task.FromResult(path.StartsWith(Protocol));
public override IContainer? GetParent() => _parent;
public void SetParent(IContainer parent) => _parent = parent;
diff --git a/src/Core/FileTime.Core/Search/ChildSearchElement.cs b/src/Core/FileTime.Core/Search/ChildSearchElement.cs
index e0ac039..d1bee98 100644
--- a/src/Core/FileTime.Core/Search/ChildSearchElement.cs
+++ b/src/Core/FileTime.Core/Search/ChildSearchElement.cs
@@ -29,7 +29,7 @@ namespace FileTime.Core.Search
public override async Task 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();
}
diff --git a/src/Core/FileTime.Core/Timeline/TimeElement.cs b/src/Core/FileTime.Core/Timeline/TimeElement.cs
index 61b704a..dc5b145 100644
--- a/src/Core/FileTime.Core/Timeline/TimeElement.cs
+++ b/src/Core/FileTime.Core/Timeline/TimeElement.cs
@@ -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;
diff --git a/src/Core/FileTime.Core/Timeline/TimeProvider.cs b/src/Core/FileTime.Core/Timeline/TimeProvider.cs
index d17d3fb..69b1f1a 100644
--- a/src/Core/FileTime.Core/Timeline/TimeProvider.cs
+++ b/src/Core/FileTime.Core/Timeline/TimeProvider.cs
@@ -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;
}
diff --git a/src/FileTime.sln b/src/FileTime.sln
index 2c4bf15..929b588 100644
--- a/src/FileTime.sln
+++ b/src/FileTime.sln
@@ -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}
diff --git a/src/GuiApp/FileTime.Avalonia/Application/AppState.cs b/src/GuiApp/FileTime.Avalonia/Application/AppState.cs
index 68accd5..e185eaf 100644
--- a/src/GuiApp/FileTime.Avalonia/Application/AppState.cs
+++ b/src/GuiApp/FileTime.Avalonia/Application/AppState.cs
@@ -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 _favoriteElements;
+
public List PreviousKeys { get; } = new();
public ObservableCollection TimelineCommands { get; } = new();
diff --git a/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs b/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs
index aa71e19..383673b 100644
--- a/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs
+++ b/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs
@@ -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),
diff --git a/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj b/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj
index 9da264b..6e76cd7 100644
--- a/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj
+++ b/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj
@@ -53,6 +53,7 @@
+
diff --git a/src/GuiApp/FileTime.Avalonia/IconProviders/MaterialIconProvider.cs b/src/GuiApp/FileTime.Avalonia/IconProviders/MaterialIconProvider.cs
index a950f55..9ca4e97 100644
--- a/src/GuiApp/FileTime.Avalonia/IconProviders/MaterialIconProvider.cs
+++ b/src/GuiApp/FileTime.Avalonia/IconProviders/MaterialIconProvider.cs
@@ -10,7 +10,6 @@ namespace FileTime.Avalonia.IconProviders
{
public class MaterialIconProvider : IIconProvider
{
-
private static readonly Dictionary _iconsByExtension = new();
private static readonly Dictionary _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
{
diff --git a/src/GuiApp/FileTime.Avalonia/Models/PlaceInfo.cs b/src/GuiApp/FileTime.Avalonia/Models/PlaceInfo.cs
index d8c8ece..13606e9 100644
--- a/src/GuiApp/FileTime.Avalonia/Models/PlaceInfo.cs
+++ b/src/GuiApp/FileTime.Avalonia/Models/PlaceInfo.cs
@@ -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)
{
diff --git a/src/GuiApp/FileTime.Avalonia/Models/RootDriveInfo.cs b/src/GuiApp/FileTime.Avalonia/Models/RootDriveInfo.cs
index 58a54de..f61cff7 100644
--- a/src/GuiApp/FileTime.Avalonia/Models/RootDriveInfo.cs
+++ b/src/GuiApp/FileTime.Avalonia/Models/RootDriveInfo.cs
@@ -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;
diff --git a/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs b/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs
index 47a3454..0364635 100644
--- a/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs
+++ b/src/GuiApp/FileTime.Avalonia/Services/CommandHandlerService.cs
@@ -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 _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 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>
{
@@ -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 GetFavoriteElements(FavoriteContainerBase container)
+ {
+ return container.Elements.Where(e => e.IsPinned).Concat(
+ container.Containers.SelectMany(GetFavoriteElements)
+ );
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/HistoryItemViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/HistoryItemViewModel.cs
index 317e701..397101f 100644
--- a/src/GuiApp/FileTime.Avalonia/ViewModels/HistoryItemViewModel.cs
+++ b/src/GuiApp/FileTime.Avalonia/ViewModels/HistoryItemViewModel.cs
@@ -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
+ public class HistoryItemViewModel : IAsyncInitable, IHaveContainer
{
public string? Name { get; private set; }
public IContainer? Container { get; private set; }
diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs
index bd718da..d2f7826 100644
--- a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs
+++ b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs
@@ -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();
_timeRunner = App.ServiceProvider.GetService()!;
var inputInterface = (BasicInputHandler)App.ServiceProvider.GetService()!;
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 GetFavoriteElements(FavoriteContainerBase container)
+ {
+ return container.Elements.Where(e => e.IsPinned).Concat(
+ container.Containers.SelectMany(GetFavoriteElements)
+ );
+ }
}
private Task UpdateParallelCommands(object? sender, IReadOnlyList parallelCommands, CancellationToken token)
@@ -269,7 +284,9 @@ namespace FileTime.Avalonia.ViewModels
public void ProcessKeyDown(Key key, KeyModifiers keyModifiers, Action 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
}
}
}
diff --git a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml
index 37620ba..04ac18a 100644
--- a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml
+++ b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml
@@ -41,7 +41,7 @@
-
+
@@ -149,6 +149,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ 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)
{
diff --git a/src/Providers/FileTime.Providers.Favorites/CommandHandlers/ToFavoriteCopyCommandHandler.cs b/src/Providers/FileTime.Providers.Favorites/CommandHandlers/ToFavoriteCopyCommandHandler.cs
new file mode 100644
index 0000000..67c64ee
--- /dev/null
+++ b/src/Providers/FileTime.Providers.Favorites/CommandHandlers/ToFavoriteCopyCommandHandler.cs
@@ -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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Providers/FileTime.Providers.Favorites/FavoriteContainer.cs b/src/Providers/FileTime.Providers.Favorites/FavoriteContainer.cs
new file mode 100644
index 0000000..38bec2a
--- /dev/null
+++ b/src/Providers/FileTime.Providers.Favorites/FavoriteContainer.cs
@@ -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)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Providers/FileTime.Providers.Favorites/FavoriteContainerBase.cs b/src/Providers/FileTime.Providers.Favorites/FavoriteContainerBase.cs
new file mode 100644
index 0000000..caca73b
--- /dev/null
+++ b/src/Providers/FileTime.Providers.Favorites/FavoriteContainerBase.cs
@@ -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 _containers;
+ private List _items;
+ private readonly List _elements;
+ private readonly List _exceptions = new();
+ private readonly PersistenceService _persistenceService;
+
+ public bool IsExists => true;
+
+ public IReadOnlyList 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 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 Containers { get; }
+ public IReadOnlyList 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();
+ _items = new List();
+ _elements = new List();
+ _persistenceService = persistenceService;
+
+ Containers = _containers.AsReadOnly();
+ Elements = _elements.AsReadOnly();
+
+ Exceptions = _exceptions.AsReadOnly();
+ DisplayName = Name = name;
+ NativePath = FullName = fullName;
+ Provider = null!;
+ }
+
+ public Task CloneAsync() => Task.FromResult((IContainer)this);
+
+ public async Task CreateContainerAsync(string name)
+ {
+ var container = new FavoriteContainer(_persistenceService, Provider, this, name);
+ await AddContainerAsync(container);
+ return container;
+ }
+
+ public Task 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().Concat(Provider.Elements.Cast()));
+ }
+
+ public async Task AddContainerAsync(FavoriteContainer container)
+ {
+ _containers.Add(container);
+ UpdateItems();
+ await SaveFavoritesAsync();
+ await RefreshAsync();
+ }
+
+ public async Task AddContainersAsync(IEnumerable 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 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().Concat(_elements).ToList();
+ }
+
+ public async Task?> GetContainers(CancellationToken token = default)
+ {
+ await InitIfNeeded();
+ return _containers;
+ }
+
+ public async Task?> GetElements(CancellationToken token = default)
+ {
+ await InitIfNeeded();
+ return _elements;
+ }
+
+ public async Task?> GetItems(CancellationToken token = default)
+ {
+ await InitIfNeeded();
+ return _items;
+ }
+
+ public virtual Task InitIfNeeded() => Task.CompletedTask;
+
+ public async Task IsExistsAsync(string name)
+ {
+ var items = await GetItems();
+ return items?.Any(i => i.Name == name) ?? false;
+ }
+
+ public virtual Task CanOpenAsync() => Task.FromResult(_exceptions.Count == 0);
+
+ public void Unload() { }
+
+ public Task HandleEscape() => throw new NotSupportedException();
+
+ public async Task RunWithLoading(Func func, CancellationToken token = default)
+ {
+ await func(token);
+ }
+
+ public void Destroy() { }
+
+ public IContainer? GetParent() => Parent;
+ }
+}
\ No newline at end of file
diff --git a/src/Providers/FileTime.Providers.Favorites/FavoriteContentProvider.cs b/src/Providers/FileTime.Providers.Favorites/FavoriteContentProvider.cs
new file mode 100644
index 0000000..c49ba9e
--- /dev/null
+++ b/src/Providers/FileTime.Providers.Favorites/FavoriteContentProvider.cs
@@ -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> _contentProvidersLazy;
+
+ public FavoriteContentProvider(PersistenceService persistenceService, IServiceProvider serviceProvider) : base(persistenceService, "favorite")
+ {
+ Protocol = "favorite://";
+ _persistenceService = persistenceService;
+ _contentProvidersLazy = new Lazy>(() => serviceProvider.GetRequiredService>());
+ }
+
+ public bool SupportsContentStreams => false;
+
+ public string Protocol { get; }
+
+ public Task 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 containerDtos,
+ IEnumerable elementDtos)
+ {
+ var newContainers = new List();
+ var newElements = new List();
+
+ 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Providers/FileTime.Providers.Favorites/FavoriteElement.cs b/src/Providers/FileTime.Providers.Favorites/FavoriteElement.cs
new file mode 100644
index 0000000..1e0d58a
--- /dev/null
+++ b/src/Providers/FileTime.Providers.Favorites/FavoriteElement.cs
@@ -0,0 +1,33 @@
+using FileTime.Core.Models;
+using FileTime.Core.Providers;
+
+namespace FileTime.Providers.Favorites
+{
+ public class FavoriteElement : AbstractElement, 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 GetContent(CancellationToken token = default) => throw new NotSupportedException();
+
+ public override Task GetContentReaderAsync() => throw new NotSupportedException();
+
+ public override Task GetContentWriterAsync() => throw new NotSupportedException();
+
+ public override Task GetElementSize(CancellationToken token = default) => Task.FromResult((long?)null);
+
+ public override string? GetPrimaryAttributeText() => null;
+
+ public override Task Rename(string newName) => throw new NotSupportedException();
+ }
+}
\ No newline at end of file
diff --git a/src/Providers/FileTime.Providers.Favorites/FileTime.Providers.Favorites.csproj b/src/Providers/FileTime.Providers.Favorites/FileTime.Providers.Favorites.csproj
new file mode 100644
index 0000000..889a469
--- /dev/null
+++ b/src/Providers/FileTime.Providers.Favorites/FileTime.Providers.Favorites.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Providers/FileTime.Providers.Favorites/IFavoriteItem.cs b/src/Providers/FileTime.Providers.Favorites/IFavoriteItem.cs
new file mode 100644
index 0000000..c7861f9
--- /dev/null
+++ b/src/Providers/FileTime.Providers.Favorites/IFavoriteItem.cs
@@ -0,0 +1,4 @@
+namespace FileTime.Providers.Favorites
+{
+ public interface IFavoriteItem { }
+}
\ No newline at end of file
diff --git a/src/Providers/FileTime.Providers.Favorites/Persistence/FavoriteContainerDto.cs b/src/Providers/FileTime.Providers.Favorites/Persistence/FavoriteContainerDto.cs
new file mode 100644
index 0000000..451b4a9
--- /dev/null
+++ b/src/Providers/FileTime.Providers.Favorites/Persistence/FavoriteContainerDto.cs
@@ -0,0 +1,42 @@
+namespace FileTime.Providers.Favorites.Persistence
+{
+ public class FavoriteContainerDto
+ {
+ public List Containers { get; set; } = new();
+ public List Elements { get; set; } = new();
+ public string Name { get; set; } = null!;
+
+ public async Task Init(FavoriteContainer favoriteContainer)
+ {
+ Name = favoriteContainer.Name;
+
+ var newContainers = new List();
+ var newElements = new List();
+
+ var containers = await favoriteContainer.GetContainers();
+ var elements = await favoriteContainer.GetElements();
+
+ if (containers != null)
+ {
+ foreach (var container in containers.Cast())
+ {
+ var childFavorite = new FavoriteContainerDto();
+ await childFavorite.Init(container);
+ newContainers.Add(childFavorite);
+ }
+ }
+
+ if (elements != null)
+ {
+ foreach (var element in elements.Cast())
+ {
+ var childFavorite = new FavoriteElementDto(element);
+ newElements.Add(childFavorite);
+ }
+ }
+
+ Containers = newContainers;
+ Elements = newElements;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Providers/FileTime.Providers.Favorites/Persistence/FavoriteElementDto.cs b/src/Providers/FileTime.Providers.Favorites/Persistence/FavoriteElementDto.cs
new file mode 100644
index 0000000..937c4d7
--- /dev/null
+++ b/src/Providers/FileTime.Providers.Favorites/Persistence/FavoriteElementDto.cs
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Providers/FileTime.Providers.Favorites/Persistence/FavoritePersistenceRoot.cs b/src/Providers/FileTime.Providers.Favorites/Persistence/FavoritePersistenceRoot.cs
new file mode 100644
index 0000000..25a5f1e
--- /dev/null
+++ b/src/Providers/FileTime.Providers.Favorites/Persistence/FavoritePersistenceRoot.cs
@@ -0,0 +1,8 @@
+namespace FileTime.Providers.Favorites.Persistence
+{
+ public class FavoritePersistenceRoot
+ {
+ public List Containers { get; set; } = new();
+ public List Elements { get; set; } = new();
+ }
+}
\ No newline at end of file
diff --git a/src/Providers/FileTime.Providers.Favorites/Persistence/PersistenceService.cs b/src/Providers/FileTime.Providers.Favorites/Persistence/PersistenceService.cs
new file mode 100644
index 0000000..d4624b9
--- /dev/null
+++ b/src/Providers/FileTime.Providers.Favorites/Persistence/PersistenceService.cs
@@ -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 items)
+ {
+ var containers = new List();
+ var elements = new List();
+
+ 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, IEnumerable)> LoadFavorites()
+ {
+ var persistencePath = Path.Combine(_persistenceSettings.RootAppDataPath, favoriteFolderName, favoriteFileName);
+
+ if (!new FileInfo(persistencePath).Exists) return (Enumerable.Empty(), Enumerable.Empty());
+
+ using var stream = File.OpenRead(persistencePath);
+ var serversRoot = (await JsonSerializer.DeserializeAsync(stream))!;
+
+ return (serversRoot.Containers, serversRoot.Elements);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Providers/FileTime.Providers.Favorites/Startup.cs b/src/Providers/FileTime.Providers.Favorites/Startup.cs
new file mode 100644
index 0000000..037243d
--- /dev/null
+++ b/src/Providers/FileTime.Providers.Favorites/Startup.cs
@@ -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()
+ .AddSingleton(serviceProvider => serviceProvider.GetRequiredService())
+ .AddSingleton()
+ .RegisterFavoriteCommandHandlers();
+ }
+
+ internal static IServiceCollection RegisterFavoriteCommandHandlers(this IServiceCollection serviceCollection)
+ {
+ var commandHandlers = new List()
+ {
+ typeof(ToFavoriteCopyCommandHandler)
+ };
+
+ foreach (var commandHandler in commandHandlers)
+ {
+ serviceCollection.AddTransient(typeof(ICommandHandler), commandHandler);
+ }
+
+ return serviceCollection;
+ }
+ }
+}
\ 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 ecfe36a..f384e42 100644
--- a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs
+++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs
@@ -11,7 +11,7 @@ namespace FileTime.Providers.Local
public bool IsCaseInsensitive { get; }
public LocalContentProvider(ILogger logger)
- : base("local", null, "local://", true)
+ : base("local", "local://", true)
{
_logger = logger;
diff --git a/src/Providers/FileTime.Providers.Local/LocalFile.cs b/src/Providers/FileTime.Providers.Local/LocalFile.cs
index e734e44..ca9bc17 100644
--- a/src/Providers/FileTime.Providers.Local/LocalFile.cs
+++ b/src/Providers/FileTime.Providers.Local/LocalFile.cs
@@ -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)
{
diff --git a/src/Providers/FileTime.Providers.Sftp/SftpContentProvider.cs b/src/Providers/FileTime.Providers.Sftp/SftpContentProvider.cs
index 90915ac..5f1feda 100644
--- a/src/Providers/FileTime.Providers.Sftp/SftpContentProvider.cs
+++ b/src/Providers/FileTime.Providers.Sftp/SftpContentProvider.cs
@@ -11,14 +11,12 @@ namespace FileTime.Providers.Sftp
private readonly ILogger _logger;
public SftpContentProvider(IInputInterface inputInterface, ILogger logger)
- : base("sftp", null, "sftp://", false)
+ : base("sftp", "sftp://", false)
{
_logger = logger;
_inputInterface = inputInterface;
}
- public override Task CanHandlePath(string path) => Task.FromResult(path.StartsWith(Protocol));
-
public override async Task CreateContainerAsync(string name)
{
var container = RootContainers?.FirstOrDefault(c => c.Name == name);
diff --git a/src/Providers/FileTime.Providers.Sftp/SftpFile.cs b/src/Providers/FileTime.Providers.Sftp/SftpFile.cs
index 39048b9..0e4e38d 100644
--- a/src/Providers/FileTime.Providers.Sftp/SftpFile.cs
+++ b/src/Providers/FileTime.Providers.Sftp/SftpFile.cs
@@ -61,7 +61,7 @@ namespace FileTime.Providers.Sftp
throw new NotImplementedException();
}
- public string GetPrimaryAttributeText()
+ public string? GetPrimaryAttributeText()
{
throw new NotImplementedException();
}
diff --git a/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs b/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs
index f4956f3..a335a08 100644
--- a/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs
+++ b/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs
@@ -13,7 +13,7 @@ namespace FileTime.Providers.Smb
private readonly ILogger _logger;
public SmbContentProvider(IInputInterface inputInterface, Persistence.PersistenceService persistenceService, ILogger logger)
- : base("smb", null, "smb://", true)
+ : base("smb", "smb://", true)
{
_inputInterface = inputInterface;
_persistenceService = persistenceService;
diff --git a/src/Providers/FileTime.Providers.Smb/SmbFile.cs b/src/Providers/FileTime.Providers.Smb/SmbFile.cs
index 4717998..3c56495 100644
--- a/src/Providers/FileTime.Providers.Smb/SmbFile.cs
+++ b/src/Providers/FileTime.Providers.Smb/SmbFile.cs
@@ -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 GetContent(CancellationToken token = default) => Task.FromResult("NotImplemented");