From 3e553dd448c5dfdb4be0d7bab807aa1753cf576e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Wed, 2 Feb 2022 11:02:37 +0100 Subject: [PATCH] IconProvider, container exceptions, refactor --- .../FileTime.App.Core/Clipboard/Clipboard.cs | 5 +- src/Core/FileTime.Core/Models/IContainer.cs | 2 + .../FileTime.Core/Models/VirtualContainer.cs | 2 + .../FileTime.Core/Providers/TopContainer.cs | 4 + .../FileTime.Core/Timeline/TimeContainer.cs | 2 + .../FileTime.Core/Timeline/TimeProvider.cs | 5 +- src/GuiApp/FileTime.Avalonia/App.axaml | 12 +- .../Converters/IsEmptyConverter.cs | 23 ++++ .../Converters/ItemToImageConverter.cs | 27 +++- .../FileTime.Avalonia.csproj | 13 +- .../IconProviders/IIconProvider.cs | 4 +- .../IconProviders/MaterialIconProvider.cs | 76 ++++++++--- .../IconProviders/SystemIconProvider.cs | 29 +++++ .../Misc/WindowsNativeMethods.cs | 73 +++++++++++ .../FileTime.Avalonia/Models/ImagePath.cs | 21 ++++ .../FileTime.Avalonia/Models/ImagePathType.cs | 9 ++ .../FileTime.Avalonia/Models/PlaceInfo.cs | 16 +++ .../FileTime.Avalonia/Models/RootDriveInfo.cs | 19 ++- .../Services/WindowsContextMenuProvider.cs | 34 +---- src/GuiApp/FileTime.Avalonia/Startup.cs | 4 +- .../ViewModels/ContainerViewModel.cs | 84 ++++++++----- .../ViewModels/MainPageViewModel.cs | 105 +++++++++++++++- .../FileTime.Avalonia/Views/ItemView.axaml | 2 +- .../FileTime.Avalonia/Views/MainWindow.axaml | 118 ++++++++++++++---- .../Views/MainWindow.axaml.cs | 16 ++- .../LocalContentProvider.cs | 2 + .../FileTime.Providers.Local/LocalFolder.cs | 12 +- .../SmbContentProvider.cs | 2 + .../FileTime.Providers.Smb/SmbFolder.cs | 2 + .../FileTime.Providers.Smb/SmbServer.cs | 4 +- .../FileTime.Providers.Smb/SmbShare.cs | 2 + 31 files changed, 597 insertions(+), 132 deletions(-) create mode 100644 src/GuiApp/FileTime.Avalonia/Converters/IsEmptyConverter.cs create mode 100644 src/GuiApp/FileTime.Avalonia/IconProviders/SystemIconProvider.cs create mode 100644 src/GuiApp/FileTime.Avalonia/Misc/WindowsNativeMethods.cs create mode 100644 src/GuiApp/FileTime.Avalonia/Models/ImagePath.cs create mode 100644 src/GuiApp/FileTime.Avalonia/Models/ImagePathType.cs create mode 100644 src/GuiApp/FileTime.Avalonia/Models/PlaceInfo.cs diff --git a/src/AppCommon/FileTime.App.Core/Clipboard/Clipboard.cs b/src/AppCommon/FileTime.App.Core/Clipboard/Clipboard.cs index e1e6e23..3542798 100644 --- a/src/AppCommon/FileTime.App.Core/Clipboard/Clipboard.cs +++ b/src/AppCommon/FileTime.App.Core/Clipboard/Clipboard.cs @@ -1,12 +1,11 @@ using FileTime.Core.Command; using FileTime.Core.Models; -using FileTime.Core.Providers; namespace FileTime.App.Core.Clipboard { public class Clipboard : IClipboard { - private readonly List _content; + private List _content; public IReadOnlyList Content { get; } public Type? CommandType { get; private set; } @@ -39,7 +38,7 @@ namespace FileTime.App.Core.Clipboard public void Clear() { - _content.Clear(); + _content = new List(); CommandType = null; } diff --git a/src/Core/FileTime.Core/Models/IContainer.cs b/src/Core/FileTime.Core/Models/IContainer.cs index d19c139..5f73be8 100644 --- a/src/Core/FileTime.Core/Models/IContainer.cs +++ b/src/Core/FileTime.Core/Models/IContainer.cs @@ -4,6 +4,7 @@ namespace FileTime.Core.Models { public interface IContainer : IItem { + IReadOnlyList Exceptions { get; } Task?> GetItems(CancellationToken token = default); Task?> GetContainers(CancellationToken token = default); Task?> GetElements(CancellationToken token = default); @@ -16,6 +17,7 @@ namespace FileTime.Core.Models Task IsExists(string name); Task Clone(); + Task CanOpen(); bool IsLoaded { get; } diff --git a/src/Core/FileTime.Core/Models/VirtualContainer.cs b/src/Core/FileTime.Core/Models/VirtualContainer.cs index e818373..dec2531 100644 --- a/src/Core/FileTime.Core/Models/VirtualContainer.cs +++ b/src/Core/FileTime.Core/Models/VirtualContainer.cs @@ -29,6 +29,7 @@ namespace FileTime.Core.Models public bool CanRename => BaseContainer.CanRename; public IContentProvider Provider => BaseContainer.Provider; + public IReadOnlyList Exceptions => BaseContainer.Exceptions; public AsyncEventHandler Refreshed { get; } @@ -164,5 +165,6 @@ namespace FileTime.Core.Models } public async Task Rename(string newName) => await BaseContainer.Rename(newName); + public async Task CanOpen() => await BaseContainer.CanOpen(); } } \ 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 50833e3..aa18c23 100644 --- a/src/Core/FileTime.Core/Providers/TopContainer.cs +++ b/src/Core/FileTime.Core/Providers/TopContainer.cs @@ -29,6 +29,8 @@ namespace FileTime.Core.Providers public AsyncEventHandler Refreshed { get; } = new(); + public IReadOnlyList Exceptions { get; } = new List().AsReadOnly(); + public TopContainer(IEnumerable contentProviders) { _contentProviders = new List(contentProviders); @@ -62,5 +64,7 @@ namespace FileTime.Core.Providers public Task Clone() => Task.FromResult((IContainer)this); public Task Rename(string newName) => throw new NotSupportedException(); + + public Task CanOpen() => Task.FromResult(true); } } \ 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 2165616..236a961 100644 --- a/src/Core/FileTime.Core/Timeline/TimeContainer.cs +++ b/src/Core/FileTime.Core/Timeline/TimeContainer.cs @@ -25,6 +25,7 @@ namespace FileTime.Core.Timeline public IContentProvider Provider { get; } public IContentProvider VirtualProvider { get; } + public IReadOnlyList Exceptions { get; } = new List().AsReadOnly(); public TimeContainer(string name, IContainer parent, IContentProvider contentProvider, IContentProvider virtualContentProvider, PointInTime pointInTime) { @@ -117,5 +118,6 @@ namespace FileTime.Core.Timeline if (elementDiff.Type != DifferenceItemType.Container) throw new ArgumentException($"{elementDiff}'s {nameof(Difference.Type)} property is not {DifferenceItemType.Element}."); return new TimeElement(elementDiff.Name, this, Provider, elementDiff.AbsolutePath.VirtualContentProvider ?? elementDiff.AbsolutePath.ContentProvider); } + public Task CanOpen() => Task.FromResult(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 229cb50..1687a3a 100644 --- a/src/Core/FileTime.Core/Timeline/TimeProvider.cs +++ b/src/Core/FileTime.Core/Timeline/TimeProvider.cs @@ -6,6 +6,8 @@ namespace FileTime.Core.Timeline { public class TimeProvider : IContentProvider { + private readonly PointInTime _pointInTime; + public bool IsLoaded => true; public AsyncEventHandler Refreshed { get; } = new(); @@ -22,7 +24,7 @@ namespace FileTime.Core.Timeline public IContentProvider Provider => this; - private readonly PointInTime _pointInTime; + public IReadOnlyList Exceptions { get; } = new List().AsReadOnly(); public TimeProvider(PointInTime pointInTime) { @@ -85,5 +87,6 @@ namespace FileTime.Core.Timeline public Task Rename(string newName) => throw new NotSupportedException(); public void SetParent(IContainer container) { } + public Task CanOpen() => Task.FromResult(true); } } \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/App.axaml b/src/GuiApp/FileTime.Avalonia/App.axaml index 202ae03..a3122c3 100644 --- a/src/GuiApp/FileTime.Avalonia/App.axaml +++ b/src/GuiApp/FileTime.Avalonia/App.axaml @@ -120,6 +120,8 @@ + + @@ -132,7 +134,7 @@ + + + + diff --git a/src/GuiApp/FileTime.Avalonia/Converters/IsEmptyConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/IsEmptyConverter.cs new file mode 100644 index 0000000..6bfd77e --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Converters/IsEmptyConverter.cs @@ -0,0 +1,23 @@ +using Avalonia.Data.Converters; +using System; +using System.Globalization; + +namespace FileTime.Avalonia.Converters +{ + public class IsEmptyConverter : IValueConverter + { + public bool Inverse { get; set; } + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var result = value is string s && string.IsNullOrWhiteSpace(s); + if (Inverse) result = !result; + return result; + } + + 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/Converters/ItemToImageConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/ItemToImageConverter.cs index 6342d3f..9b9301a 100644 --- a/src/GuiApp/FileTime.Avalonia/Converters/ItemToImageConverter.cs +++ b/src/GuiApp/FileTime.Avalonia/Converters/ItemToImageConverter.cs @@ -5,26 +5,45 @@ using Avalonia.Svg.Skia; using FileTime.Avalonia.IconProviders; using FileTime.Avalonia.ViewModels; using FileTime.Core.Models; +using Microsoft.Extensions.DependencyInjection; namespace FileTime.Avalonia.Converters { public class ItemToImageConverter : IValueConverter { + private readonly IIconProvider _iconProvider; + + public ItemToImageConverter() + { + _iconProvider = App.ServiceProvider.GetService()!; + } + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value == null) return null; - IIconProvider converter = new MaterialIconProvider(); - IItem item = value switch { ContainerViewModel container => container.Container, ElementViewModel element => element.Element, + IItem i => i, _ => throw new NotImplementedException() }; - var path = converter.GetImage(item)!; - var source = SvgSource.Load("avares://FileTime.Avalonia" + path, null); + SvgSource? source; + var path = _iconProvider.GetImage(item)!; + if (path.Type == Models.ImagePathType.Absolute) + { + source = SvgSource.Load(path.Path!, null); + } + else if(path.Type == Models.ImagePathType.Raw) + { + return path.Image; + } + else + { + source = SvgSource.Load("avares://FileTime.Avalonia" + path.Path, null); + } return new SvgImage { Source = source }; } diff --git a/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj b/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj index 16058f7..0146552 100644 --- a/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj +++ b/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj @@ -11,16 +11,17 @@ - - + + - - - - + + + + + diff --git a/src/GuiApp/FileTime.Avalonia/IconProviders/IIconProvider.cs b/src/GuiApp/FileTime.Avalonia/IconProviders/IIconProvider.cs index d24c70d..0056b4f 100644 --- a/src/GuiApp/FileTime.Avalonia/IconProviders/IIconProvider.cs +++ b/src/GuiApp/FileTime.Avalonia/IconProviders/IIconProvider.cs @@ -1,9 +1,11 @@ +using FileTime.Avalonia.Models; using FileTime.Core.Models; namespace FileTime.Avalonia.IconProviders { public interface IIconProvider { - string GetImage(IItem item); + ImagePath GetImage(IItem item); + bool EnableAdvancedIcons { get; set; } } } \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/IconProviders/MaterialIconProvider.cs b/src/GuiApp/FileTime.Avalonia/IconProviders/MaterialIconProvider.cs index c42e90a..6c2fdab 100644 --- a/src/GuiApp/FileTime.Avalonia/IconProviders/MaterialIconProvider.cs +++ b/src/GuiApp/FileTime.Avalonia/IconProviders/MaterialIconProvider.cs @@ -1,33 +1,75 @@ +using Avalonia.Media.Imaging; +using FileTime.Avalonia.Misc; +using FileTime.Avalonia.Models; using FileTime.Core.Models; using FileTime.Providers.Local; +using System; +using System.IO; using System.Linq; +using System.Runtime.InteropServices; namespace FileTime.Avalonia.IconProviders { public class MaterialIconProvider : IIconProvider { - public string GetImage(IItem item) + public bool EnableAdvancedIcons { get; set; } = true; + + public ImagePath GetImage(IItem item) { - var icon = "file.svg"; - if (item is IContainer) + var icon = item is IContainer ? "folder.svg" : "file.svg"; + + if (EnableAdvancedIcons) { - icon = "folder.svg"; - } - else if (item is IElement element) - { - if(element is LocalFile localFile && element.FullName.EndsWith(".svg")) + if (item is IElement element) { - return localFile.File.FullName; - } - icon = !element.Name.Contains('.') - ? icon - : element.Name.Split('.').Last() switch + if (element is LocalFile localFile && (element.FullName?.EndsWith(".svg") ?? false)) { - "cs" => "csharp.svg", - _ => icon - }; + return new ImagePath(ImagePathType.Absolute, localFile.File.FullName); + } + icon = !element.Name.Contains('.') + ? icon + : element.Name.Split('.').Last() switch + { + "cs" => "csharp.svg", + _ => icon + }; + } + /*else if (item is LocalFolder folder && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var file = new FileInfo(Path.Combine(folder.FullName, "desktop.ini")); + if (file.Exists) + { + var lines = File.ReadAllLines(file.FullName); + if (Array.Find(lines, l => l.StartsWith("iconresource", StringComparison.OrdinalIgnoreCase)) is string iconLine) + { + var nameLineValue = string.Join('=', iconLine.Split('=')[1..]); + var environemntVariables = Environment.GetEnvironmentVariables(); + foreach (var keyo in environemntVariables.Keys) + { + if (keyo is string key && environemntVariables[key] is string value) + { + nameLineValue = nameLineValue.Replace($"%{key}%", value); + } + } + + var parts = nameLineValue.Split(','); + if (parts.Length >= 2 && long.TryParse(parts[^1], out var parsedResourceId)) + { + if (parsedResourceId < 0) parsedResourceId *= -1; + + var extractedIcon = NativeMethodHelpers.GetIconResource(string.Join(',', parts[..^1]), (uint)parsedResourceId); + + var extractedIconAsStream = new MemoryStream(); + extractedIcon.Save(extractedIconAsStream); + extractedIconAsStream.Position = 0; + + return new ImagePath(ImagePathType.Raw, new Bitmap(extractedIconAsStream)); + } + } + } + }*/ } - return "/Assets/material/" + icon; + return new ImagePath(ImagePathType.Asset, "/Assets/material/" + icon); } } } diff --git a/src/GuiApp/FileTime.Avalonia/IconProviders/SystemIconProvider.cs b/src/GuiApp/FileTime.Avalonia/IconProviders/SystemIconProvider.cs new file mode 100644 index 0000000..4a22049 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/IconProviders/SystemIconProvider.cs @@ -0,0 +1,29 @@ +using System.IO; +using Avalonia.Media.Imaging; +using FileTime.Avalonia.Misc; +using FileTime.Avalonia.Models; +using FileTime.Core.Models; +using FileTime.Providers.Local; + +namespace FileTime.Avalonia.IconProviders +{ + public class SystemIconProvider : IIconProvider + { + public bool EnableAdvancedIcons { get; set; } + + public ImagePath GetImage(IItem item) + { + if (item is LocalFile file) + { + var extractedIconAsStream = new MemoryStream(); + var extractedIcon = System.Drawing.Icon.ExtractAssociatedIcon(file.File.FullName); + extractedIcon.Save(extractedIconAsStream); + extractedIconAsStream.Position = 0; + return new ImagePath(ImagePathType.Raw, new Bitmap(extractedIconAsStream)); + } + + var icon = item is IContainer ? "folder.svg" : "file.svg"; + return new ImagePath(ImagePathType.Asset, "/Assets/material/" + icon); + } + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Misc/WindowsNativeMethods.cs b/src/GuiApp/FileTime.Avalonia/Misc/WindowsNativeMethods.cs new file mode 100644 index 0000000..b2658eb --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Misc/WindowsNativeMethods.cs @@ -0,0 +1,73 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Text; + +namespace FileTime.Avalonia.Misc +{ + public static class WindowsNativeMethods + { + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] + internal static extern IntPtr LoadLibrary(string lpLibFileName); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] + internal static extern int LoadString(IntPtr hInstance, uint wID, StringBuilder lpBuffer, int nBufferMax); + + [DllImport("user32.dll")] + public static extern IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName); + + [DllImport("kernel32.dll")] + public static extern int FreeLibrary(IntPtr hLibModule); + + [DllImport("shell32.dll")] + public static extern IntPtr ExtractAssociatedIcon(IntPtr hInst, StringBuilder lpIconPath, out ushort lpiIcon); + } + + public static class NativeMethodHelpers + { + public static string GetStringResource(string fileName, uint resourceId) + { + IntPtr? handle = null; + try + { + handle = WindowsNativeMethods.LoadLibrary(fileName); + StringBuilder buffer = new(8192); //Buffer for output from LoadString() + int length = WindowsNativeMethods.LoadString(handle.Value, resourceId, buffer, buffer.Capacity); + return buffer.ToString(0, length); //Return the part of the buffer that was used. + } + finally + { + if (handle is IntPtr validHandle) + { + WindowsNativeMethods.FreeLibrary(validHandle); + } + } + } + + public static Icon GetIconResource(string fileName, uint resourceId) + { + IntPtr? handle = null; + try + { + handle = WindowsNativeMethods.LoadLibrary(fileName); + IntPtr handle2 = WindowsNativeMethods.LoadIcon(handle.Value, new IntPtr(resourceId)); + return Icon.FromHandle(handle2); + } + finally + { + if (handle is IntPtr validHandle) + { + WindowsNativeMethods.FreeLibrary(validHandle); + } + } + } + + /*public static Icon GetAssociatedIcon() + { + ushort uicon; + StringBuilder strB = new StringBuilder(fileName); + IntPtr handle = WindowsNativeMethods.ExtractAssociatedIcon(this.Handle, strB, out uicon); + return Icon.FromHandle(handle); + }*/ + } +} diff --git a/src/GuiApp/FileTime.Avalonia/Models/ImagePath.cs b/src/GuiApp/FileTime.Avalonia/Models/ImagePath.cs new file mode 100644 index 0000000..70e3b61 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Models/ImagePath.cs @@ -0,0 +1,21 @@ +namespace FileTime.Avalonia.Models +{ + public class ImagePath + { + public string? Path { get; } + public ImagePathType Type { get; } + public object? Image { get; } + + public ImagePath(ImagePathType type, string path) + { + Path = path; + Type = type; + } + + public ImagePath(ImagePathType type, object image) + { + Image = image; + Type = type; + } + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Models/ImagePathType.cs b/src/GuiApp/FileTime.Avalonia/Models/ImagePathType.cs new file mode 100644 index 0000000..2b862e3 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Models/ImagePathType.cs @@ -0,0 +1,9 @@ +namespace FileTime.Avalonia.Models +{ + public enum ImagePathType + { + Asset, + Absolute, + Raw + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Models/PlaceInfo.cs b/src/GuiApp/FileTime.Avalonia/Models/PlaceInfo.cs new file mode 100644 index 0000000..035a653 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Models/PlaceInfo.cs @@ -0,0 +1,16 @@ +using FileTime.Core.Models; + +namespace FileTime.Avalonia.Models +{ + public class PlaceInfo + { + public string Name { get; } + public IContainer Container { get; } + + public PlaceInfo(string name, IContainer container) + { + Name = name; + Container = container; + } + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Models/RootDriveInfo.cs b/src/GuiApp/FileTime.Avalonia/Models/RootDriveInfo.cs index 76483ea..088b016 100644 --- a/src/GuiApp/FileTime.Avalonia/Models/RootDriveInfo.cs +++ b/src/GuiApp/FileTime.Avalonia/Models/RootDriveInfo.cs @@ -19,6 +19,9 @@ namespace FileTime.Avalonia.Models [Property] private string _fullName; + [Property] + private string _label; + [Property] private long _size; @@ -37,11 +40,17 @@ namespace FileTime.Avalonia.Models _driveInfo = driveInfo; _container = container; - Name = container.Name; - FullName = container.FullName; - Size = driveInfo.TotalSize; - Free = driveInfo.AvailableFreeSpace; - Used = driveInfo.TotalSize - driveInfo.AvailableFreeSpace; + Refresh(); + } + + private void Refresh() + { + Name = _container.Name; + FullName = _container.FullName; + Label = _driveInfo.VolumeLabel; + Size = _driveInfo.TotalSize; + Free = _driveInfo.AvailableFreeSpace; + Used = _driveInfo.TotalSize - _driveInfo.AvailableFreeSpace; } } } diff --git a/src/GuiApp/FileTime.Avalonia/Services/WindowsContextMenuProvider.cs b/src/GuiApp/FileTime.Avalonia/Services/WindowsContextMenuProvider.cs index b9f07dc..696cff3 100644 --- a/src/GuiApp/FileTime.Avalonia/Services/WindowsContextMenuProvider.cs +++ b/src/GuiApp/FileTime.Avalonia/Services/WindowsContextMenuProvider.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Text; using System.Diagnostics; using System.IO; +using FileTime.Avalonia.Misc; #pragma warning disable CA1416 namespace FileTime.Avalonia.Services @@ -50,7 +51,7 @@ namespace FileTime.Avalonia.Services { if (parsedResourceId < 0) parsedResourceId *= -1; - text = GetStringResource(parts[0], (uint)parsedResourceId); + text = NativeMethodHelpers.GetStringResource(string.Join(',', parts[..^1]), (uint)parsedResourceId); } } else @@ -231,37 +232,6 @@ namespace FileTime.Avalonia.Services return (resultX, resultY); } - - static string GetStringResource(string fileName, uint resourceId) - { - IntPtr? handle = null; - try - { - handle = NativeMethods.LoadLibrary(fileName); - StringBuilder buffer = new(8192); //Buffer for output from LoadString() - int length = NativeMethods.LoadString(handle.Value, resourceId, buffer, buffer.Capacity); - return buffer.ToString(0, length); //Return the part of the buffer that was used. - } - finally - { - if (handle is IntPtr validHandle) - { - NativeMethods.FreeLibrary(validHandle); - } - } - } - - static class NativeMethods - { - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] - internal static extern IntPtr LoadLibrary(string lpLibFileName); - - [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] - internal static extern int LoadString(IntPtr hInstance, uint wID, StringBuilder lpBuffer, int nBufferMax); - - [DllImport("kernel32.dll")] - public static extern int FreeLibrary(IntPtr hLibModule); - } } } #pragma warning restore CA1416 \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Startup.cs b/src/GuiApp/FileTime.Avalonia/Startup.cs index a0e8666..6a75ab1 100644 --- a/src/GuiApp/FileTime.Avalonia/Startup.cs +++ b/src/GuiApp/FileTime.Avalonia/Startup.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; using FileTime.Avalonia.Application; +using FileTime.Avalonia.IconProviders; using FileTime.Avalonia.Services; using FileTime.Avalonia.ViewModels; using FileTime.Core.Command; @@ -21,7 +22,8 @@ namespace FileTime.Avalonia { serviceCollection = serviceCollection .AddLogging() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs index 10987e1..4db83ce 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs @@ -20,7 +20,7 @@ namespace FileTime.Avalonia.ViewModels private bool _disposed; private bool _isRefreshing; private bool _isInitialized; - private INewItemProcessor _newItemProcessor; + private readonly INewItemProcessor _newItemProcessor; [Property] private IContainer _container; @@ -37,17 +37,19 @@ namespace FileTime.Avalonia.ViewModels [Property] private ContainerViewModel? _parent; + [Property] + private List _exceptions; + public IItem Item => _container; - private readonly ObservableCollection _containers = new ObservableCollection(); + private ObservableCollection _containers = new(); - private readonly ObservableCollection _elements = new ObservableCollection(); + private ObservableCollection _elements = new(); - private readonly ObservableCollection _items = new ObservableCollection(); + private ObservableCollection _items = new(); public List ChildrenToAdopt { get; } = new List(); - [PropertyInvalidate(nameof(IsSelected))] [PropertyInvalidate(nameof(IsAlternative))] [PropertyInvalidate(nameof(IsMarked))] @@ -64,7 +66,7 @@ namespace FileTime.Avalonia.ViewModels public List DisplayName => ItemNameConverterService.GetDisplayName(this); - [Obsolete] + [Obsolete($"This property is for databinding only, use {nameof(GetContainers)} method instead.")] public ObservableCollection Containers { get @@ -72,9 +74,17 @@ namespace FileTime.Avalonia.ViewModels if (!_isInitialized) Task.Run(Refresh); return _containers; } + set + { + if (value != _containers) + { + _containers = value; + OnPropertyChanged(nameof(Containers)); + } + } } - [Obsolete] + [Obsolete($"This property is for databinding only, use {nameof(GetElements)} method instead.")] public ObservableCollection Elements { get @@ -82,9 +92,16 @@ namespace FileTime.Avalonia.ViewModels if (!_isInitialized) Task.Run(Refresh); return _elements; } + set + { + if (value != _elements) + { + _elements = value; + OnPropertyChanged(nameof(Elements)); + } + } } - - [Obsolete] + [Obsolete($"This property is for databinding only, use {nameof(GetItems)} method instead.")] public ObservableCollection Items { get @@ -92,6 +109,14 @@ namespace FileTime.Avalonia.ViewModels if (!_isInitialized) Task.Run(Refresh); return _items; } + set + { + if (value != _items) + { + _items = value; + OnPropertyChanged(nameof(Items)); + } + } } public ContainerViewModel(INewItemProcessor newItemProcessor, ContainerViewModel? parent, IContainer container, ItemNameConverterService itemNameConverterService) : this(itemNameConverterService) @@ -125,44 +150,41 @@ namespace FileTime.Avalonia.ViewModels _isInitialized = true; + Exceptions = new List(); try { _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(); + Exceptions = new List(_container.Exceptions); - var containersToRemove = _containers.Except(containers); - - _containers.Clear(); - _elements.Clear(); - _items.Clear(); - - foreach (var containerToRemove in containersToRemove) + foreach (var containerToRemove in _containers.Except(containers)) { containerToRemove?.Dispose(); } - foreach (var container in containers) + if (initializeChildren) { - if (initializeChildren) await container.Init(false); - - _containers.Add(container); - _items.Add(container); - } - - foreach (var element in elements) - { - _elements.Add(element); - _items.Add(element); + foreach (var container in containers) + { + await container.Init(false); + } } for (var i = 0; i < _items.Count; i++) { _items[i].IsAlternative = i % 2 == 1; } + + Containers = new ObservableCollection(containers); + Elements = new ObservableCollection(elements); + Items = new ObservableCollection(containers.Cast().Concat(elements)); + } + catch (Exception e) + { + _exceptions.Add(e); } - catch { } await _newItemProcessor.UpdateMarkedItems(this); @@ -193,9 +215,9 @@ namespace FileTime.Avalonia.ViewModels } } - _containers.Clear(); - _elements.Clear(); - _items.Clear(); + _containers = new ObservableCollection(); + _elements = new ObservableCollection(); + _items = new ObservableCollection(); } public async Task> GetContainers() diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs index 66a72a3..ace11db 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs @@ -23,6 +23,9 @@ using Microsoft.Extensions.DependencyInjection; using FileTime.Core.Command; using FileTime.Core.Timeline; using FileTime.Core.Providers; +using Syroot.Windows.IO; +using FileTime.Avalonia.IconProviders; +using Avalonia.Threading; namespace FileTime.Avalonia.ViewModels { @@ -42,6 +45,8 @@ namespace FileTime.Avalonia.ViewModels private IClipboard _clipboard; private TimeRunner _timeRunner; private IEnumerable _contentProviders; + private IIconProvider _iconProvider; + private Func? _inputHandler; [Property] @@ -59,9 +64,15 @@ namespace FileTime.Avalonia.ViewModels [Property] private List _rootDriveInfos; + [Property] + private List _places; + [Property] private string _messageBoxText; + [Property] + private ObservableCollection _popupTexts = new ObservableCollection(); + public IReadOnlyList TimelineCommands => _timeRunner.ParallelCommands; async partial void OnInitialize() @@ -69,8 +80,9 @@ namespace FileTime.Avalonia.ViewModels _clipboard = App.ServiceProvider.GetService()!; _timeRunner = App.ServiceProvider.GetService()!; _contentProviders = App.ServiceProvider.GetService>()!; + _iconProvider = App.ServiceProvider.GetService()!; var inputInterface = (BasicInputHandler)App.ServiceProvider.GetService()!; - inputInterface.InputHandler = ReadInputs2; + inputInterface.InputHandler = ReadInputs; App.ServiceProvider.GetService(); _timeRunner.CommandsChanged += (o, e) => OnPropertyChanged(nameof(TimelineCommands)); @@ -103,8 +115,68 @@ namespace FileTime.Avalonia.ViewModels driveInfos.Add(driveInfo); } } - RootDriveInfos = driveInfos.OrderBy(d => d.Name).ToList(); + + var places = new List(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var placesFolders = new List() + { + KnownFolders.Profile, + KnownFolders.Desktop, + KnownFolders.DocumentsLocalized, + KnownFolders.DownloadsLocalized, + KnownFolders.Music, + KnownFolders.Pictures, + KnownFolders.Videos, + }; + + foreach (var placesFolder in placesFolders) + { + var possibleContainer = await LocalContentProvider.GetByPath(placesFolder.Path); + if (possibleContainer is IContainer container) + { + var name = container.Name; + if (await container.GetByPath("desktop.ini") is LocalFile element) + { + var lines = File.ReadAllLines(element.File.FullName); + if (Array.Find(lines, l => l.StartsWith("localizedresourcename", StringComparison.OrdinalIgnoreCase)) is string nameLine) + { + var nameLineValue = string.Join('=', nameLine.Split('=')[1..]); + var environemntVariables = Environment.GetEnvironmentVariables(); + foreach (var keyo in environemntVariables.Keys) + { + if (keyo is string key && environemntVariables[key] is string value) + { + nameLineValue = nameLineValue.Replace($"%{key}%", value); + } + } + + if (nameLineValue.StartsWith("@")) + { + var parts = nameLineValue[1..].Split(','); + if (parts.Length >= 2 && long.TryParse(parts[^1], out var parsedResourceId)) + { + if (parsedResourceId < 0) parsedResourceId *= -1; + + name = NativeMethodHelpers.GetStringResource(string.Join(',', parts[..^1]), (uint)parsedResourceId); + } + } + else + { + name = nameLineValue; + } + } + } + places.Add(new PlaceInfo(name, container)); + } + } + } + else + { + throw new Exception("TODO linux places"); + } + Places = places; } private async Task GetContainerForWindowsDrive(DriveInfo drive) @@ -123,6 +195,12 @@ namespace FileTime.Avalonia.ViewModels await AppState.SelectedTab.Open(); } + public async Task OpenContainer(IContainer container) + { + AppState.RapidTravelText = ""; + await AppState.SelectedTab.OpenContainer(container); + } + public async Task OpenOrRun() { if (AppState.SelectedTab.SelectedItem is ContainerViewModel) @@ -530,6 +608,20 @@ namespace FileTime.Avalonia.ViewModels return Task.CompletedTask; } + private Task ToggleAdvancedIcons() + { + _iconProvider.EnableAdvancedIcons = !_iconProvider.EnableAdvancedIcons; + var text = "Advanced icons are: " + (_iconProvider.EnableAdvancedIcons ? "ON" : "OFF"); + _popupTexts.Add(text); + + Task.Run(async () => + { + await Task.Delay(5000); + await Dispatcher.UIThread.InvokeAsync(() => _popupTexts.Remove(text)); + }); + return Task.CompletedTask; + } + [Command] public async void ProcessInputs() { @@ -714,7 +806,7 @@ namespace FileTime.Avalonia.ViewModels _inputHandler = inputHandler; } - public async Task ReadInputs2(IEnumerable fields) + public async Task ReadInputs(IEnumerable fields) { var waiting = true; var result = new string[0]; @@ -912,9 +1004,14 @@ namespace FileTime.Avalonia.ViewModels RefreshCurrentLocation), new CommandBinding( "go to", - FileTime.App.Core.Command.Commands.Refresh, + FileTime.App.Core.Command.Commands.Dummy, new KeyWithModifiers[]{new KeyWithModifiers(Key.L, ctrl: true)}, GoToContainer), + new CommandBinding( + "toggle advanced icons", + FileTime.App.Core.Command.Commands.Dummy, + new KeyWithModifiers[]{new KeyWithModifiers(Key.Z),new KeyWithModifiers(Key.I)}, + ToggleAdvancedIcons), }; var universalCommandBindings = new List() { diff --git a/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml b/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml index eb16276..ac02581 100644 --- a/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml +++ b/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml @@ -40,7 +40,7 @@ - + diff --git a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml index 83b639f..4091fb0 100644 --- a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml +++ b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml @@ -27,7 +27,6 @@ - @@ -36,7 +35,6 @@ - + + + + + + - + - + - + @@ -79,8 +87,40 @@ Grid.Column="1" Grid.ColumnSpan="2" Grid.Row="2" - Maximum="{Binding Size}" - Value="{Binding Used}" /> + Maximum="100" + Value="{Binding UsedPercentage}" /> + + + + + + + + + + + + + + + + + + + + + @@ -234,15 +274,27 @@ - - Empty - + + + + Empty + + + + + + + + + + @@ -314,6 +366,28 @@ Content="No" /> + + + + + + + + + + + diff --git a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml.cs b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml.cs index f07f727..188e216 100644 --- a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml.cs +++ b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml.cs @@ -4,6 +4,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using FileTime.Avalonia.Misc; +using FileTime.Avalonia.Models; using FileTime.Avalonia.ViewModels; using System.Linq; @@ -86,12 +87,25 @@ namespace FileTime.Avalonia.Views } } - private void InputText_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs args) + private void InputText_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) { if (sender is TextBox inputText && inputText.DataContext is InputElementWrapper inputElementWrapper && inputElementWrapper == ViewModel!.Inputs.First()) { inputText.Focus(); } } + + private void OnPlacePointerPressed(object sender, PointerPressedEventArgs e) + { + if(!e.Handled + && ViewModel != null + && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed + && sender is StyledElement control + && control.DataContext is PlaceInfo placeInfo) + { + ViewModel.OpenContainer(placeInfo.Container).Wait(); + e.Handled = true; + } + } } } \ 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 7db04b8..cd52962 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs @@ -29,6 +29,7 @@ namespace FileTime.Providers.Local public bool IsCaseInsensitive { get; } public bool CanDelete => false; public bool CanRename => false; + public IReadOnlyList Exceptions { get; } = new List().AsReadOnly(); public LocalContentProvider(ILogger logger) { @@ -93,5 +94,6 @@ namespace FileTime.Providers.Local public Task?> GetElements(CancellationToken token = default) => Task.FromResult(_elements); public Task Rename(string newName) => throw new NotSupportedException(); + public Task CanOpen() => Task.FromResult(true); } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Local/LocalFolder.cs b/src/Providers/FileTime.Providers.Local/LocalFolder.cs index 5582c9a..5d62216 100644 --- a/src/Providers/FileTime.Providers.Local/LocalFolder.cs +++ b/src/Providers/FileTime.Providers.Local/LocalFolder.cs @@ -10,6 +10,7 @@ namespace FileTime.Providers.Local private IReadOnlyList? _items; private IReadOnlyList? _containers; private IReadOnlyList? _elements; + private List _exceptions; private readonly IContainer? _parent; public bool IsHidden => (Directory.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; @@ -30,12 +31,16 @@ namespace FileTime.Providers.Local public string Attributes => GetAttributes(); public DateTime CreatedAt => Directory.CreationTime; + public IReadOnlyList Exceptions { get; } public LocalFolder(DirectoryInfo directory, LocalContentProvider contentProvider, IContainer? parent) { Directory = directory; _parent = parent; + _exceptions = new List(); + Exceptions = _exceptions.AsReadOnly(); + Name = directory.Name.TrimEnd(Path.DirectorySeparatorChar); FullName = parent?.FullName == null ? Name : parent.FullName + Constants.SeparatorChar + Name; Provider = contentProvider; @@ -49,13 +54,17 @@ namespace FileTime.Providers.Local { _containers = new List(); _elements = new List(); + _exceptions.Clear(); try { _containers = Directory.GetDirectories().Select(d => new LocalFolder(d, Provider, this)).OrderBy(d => d.Name).ToList().AsReadOnly(); _elements = Directory.GetFiles().Select(f => new LocalFile(f, this, Provider)).OrderBy(f => f.Name).ToList().AsReadOnly(); } - catch { } + catch (Exception e) + { + _exceptions.Add(e); + } _items = _containers.Cast().Concat(_elements).ToList().AsReadOnly(); Refreshed?.InvokeAsync(this, AsyncEventArgs.Empty); @@ -144,5 +153,6 @@ namespace FileTime.Providers.Local + ((Directory.Attributes & FileAttributes.System) == FileAttributes.System ? "s" : "-"); } } + public Task CanOpen() => Task.FromResult(true); } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs b/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs index a99ebdc..4f43d81 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs @@ -25,6 +25,7 @@ namespace FileTime.Providers.Smb public IContentProvider Provider => this; public bool CanDelete => false; public bool CanRename => false; + public IReadOnlyList Exceptions { get; } = new List().AsReadOnly(); public AsyncEventHandler Refreshed { get; } = new(); @@ -98,5 +99,6 @@ namespace FileTime.Providers.Smb public Task?> GetElements(CancellationToken token = default) => Task.FromResult(_elements); public Task Rename(string newName) => throw new NotSupportedException(); + public Task CanOpen() => Task.FromResult(true); } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Smb/SmbFolder.cs b/src/Providers/FileTime.Providers.Smb/SmbFolder.cs index 311932b..34a5d10 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbFolder.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbFolder.cs @@ -27,6 +27,7 @@ namespace FileTime.Providers.Smb public bool CanRename => true; public AsyncEventHandler Refreshed { get; } = new(); + public IReadOnlyList Exceptions { get; } = new List().AsReadOnly(); public SmbFolder(string name, SmbContentProvider contentProvider, SmbShare smbShare, IContainer parent) { @@ -119,5 +120,6 @@ namespace FileTime.Providers.Smb if (_elements == null) await Refresh(); return _elements; } + public Task CanOpen() => Task.FromResult(true); } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Smb/SmbServer.cs b/src/Providers/FileTime.Providers.Smb/SmbServer.cs index 5f32073..f1faca2 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbServer.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbServer.cs @@ -32,8 +32,9 @@ namespace FileTime.Providers.Smb public SmbContentProvider Provider { get; } IContentProvider IItem.Provider => Provider; - public bool CanDelete => false; + public bool CanDelete => true; public bool CanRename => false; + public IReadOnlyList Exceptions { get; } = new List().AsReadOnly(); public AsyncEventHandler Refreshed { get; } = new(); @@ -186,5 +187,6 @@ namespace FileTime.Providers.Smb } public Task Rename(string newName) => throw new NotSupportedException(); + public Task CanOpen() => Task.FromResult(true); } } \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Smb/SmbShare.cs b/src/Providers/FileTime.Providers.Smb/SmbShare.cs index e59820d..8d1a222 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbShare.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbShare.cs @@ -27,6 +27,7 @@ namespace FileTime.Providers.Smb public bool CanRename => false; public AsyncEventHandler Refreshed { get; } = new(); + public IReadOnlyList Exceptions { get; } = new List().AsReadOnly(); public SmbShare(string name, SmbContentProvider contentProvider, IContainer parent, SmbClientContext smbClientContext) { @@ -156,5 +157,6 @@ namespace FileTime.Providers.Smb } public Task Rename(string newName) => throw new NotSupportedException(); + public Task CanOpen() => Task.FromResult(true); } } \ No newline at end of file