diff --git a/src/GuiApp/FileTime.Avalonia/App.axaml b/src/GuiApp/FileTime.Avalonia/App.axaml index 8bfcc6d..4720bdb 100644 --- a/src/GuiApp/FileTime.Avalonia/App.axaml +++ b/src/GuiApp/FileTime.Avalonia/App.axaml @@ -2,57 +2,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:FileTime.Avalonia" xmlns:converters="using:FileTime.Avalonia.Converters" + xmlns:views="using:FileTime.Avalonia.Views" x:Class="FileTime.Avalonia.App"> - - - - - - - - - - + + @@ -64,6 +17,8 @@ #93a1a1 #93a1a1 + #268bd2 + #7793a1a1 #93a1a1 #073642 @@ -74,9 +29,9 @@ - - - + + + @@ -97,6 +52,12 @@ + + @@ -108,11 +69,48 @@ x:Key="ErrorBrush" Color="{DynamicResource ErrorColor}" /> + + + - + + + + + + + + + + + + + diff --git a/src/GuiApp/FileTime.Avalonia/Application/AppState.cs b/src/GuiApp/FileTime.Avalonia/Application/AppState.cs index 08dae28..bd1b6e8 100644 --- a/src/GuiApp/FileTime.Avalonia/Application/AppState.cs +++ b/src/GuiApp/FileTime.Avalonia/Application/AppState.cs @@ -2,6 +2,7 @@ using MvvmGen; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Text; namespace FileTime.Avalonia.Application @@ -10,8 +11,7 @@ namespace FileTime.Avalonia.Application public partial class AppState { [Property] - [PropertyCallMethod(nameof(TabsChanged))] - private List _tabs = new List(); + private ObservableCollection _tabs = new ObservableCollection(); [Property] private TabContainer _selectedTab; @@ -22,9 +22,9 @@ namespace FileTime.Avalonia.Application [Property] private string _rapidTravelText = ""; - private void TabsChanged() + partial void OnInitialize() { - SelectedTab ??= Tabs[0]; + _tabs.CollectionChanged += (o, e) => SelectedTab ??= Tabs.Count > 0 ? Tabs[0] : null; } } } diff --git a/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs b/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs index 35cdd40..61eff10 100644 --- a/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs +++ b/src/GuiApp/FileTime.Avalonia/Application/TabContainer.cs @@ -29,6 +29,12 @@ namespace FileTime.Avalonia.Application [Property] private ContainerViewModel _childContainer; + [Property] + private int _tabNumber; + + [Property] + private bool _isSelected; + private IItemViewModel? _selectedItem; public IItemViewModel? SelectedItem @@ -36,7 +42,7 @@ namespace FileTime.Avalonia.Application get => _selectedItem; set { - if (_selectedItem != value && value != null) + if (_selectedItem != value)// && value != null { _selectedItem = value; OnPropertyChanged("SelectedItem"); @@ -45,8 +51,9 @@ namespace FileTime.Avalonia.Application } } - public async Task Init() + public async Task Init(int tabNumber) { + TabNumber = tabNumber; Tab.CurrentLocationChanged.Add(Tab_CurrentLocationChanged); Tab.CurrentSelectedItemChanged.Add(Tab_CurrentSelectedItemChanged); diff --git a/src/GuiApp/FileTime.Avalonia/Converters/ContextMenuGenerator.cs b/src/GuiApp/FileTime.Avalonia/Converters/ContextMenuGenerator.cs new file mode 100644 index 0000000..b000c52 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Converters/ContextMenuGenerator.cs @@ -0,0 +1,34 @@ +using Avalonia.Controls; +using Avalonia.Data.Converters; +using FileTime.Avalonia.Services; +using FileTime.Avalonia.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Globalization; + +namespace FileTime.Avalonia.Converters +{ + public class ContextMenuGenerator : IValueConverter + { + private readonly IContextMenuProvider _contextMenuProvider; + + public ContextMenuGenerator() + { + _contextMenuProvider = App.ServiceProvider.GetService() ?? throw new Exception($"No {nameof(IContextMenuProvider)} is registered."); + } + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is ContainerViewModel containerViewModel) + { + return _contextMenuProvider.GetContextMenuForFolder(containerViewModel.Container); + } + + return new object[] { new MenuItem() { Header = "asd" } }; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/GuiApp/FileTime.Avalonia/Converters/ItemViewModeToBrushConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/ItemViewModeToBrushConverter.cs new file mode 100644 index 0000000..4eed0c7 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Converters/ItemViewModeToBrushConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; +using FileTime.Avalonia.ViewModels; + +namespace FileTime.Avalonia.Converters +{ + public class ItemViewModeToBrushConverter : IValueConverter + { + public Brush? DefaultBrush { get; set; } + public Brush? AlternativeBrush { get; set; } + public Brush? SelectedBrush { get; set; } + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is ItemViewMode viewMode) + { + return viewMode switch + { + ItemViewMode.Default => DefaultBrush, + ItemViewMode.Alternative => AlternativeBrush, + ItemViewMode.Selected => SelectedBrush, + _ => throw new NotImplementedException() + }; + } + + return value; + } + + 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/SplitStringConverter.cs b/src/GuiApp/FileTime.Avalonia/Converters/SplitStringConverter.cs new file mode 100644 index 0000000..3ff9810 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Converters/SplitStringConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace FileTime.Avalonia.Converters +{ + public class SplitStringConverter : IValueConverter + { + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is string path && parameter is string separator) + { + return path.Split(separator); + } + else if (value is string path2 && parameter is char separator2) + { + return path2.Split(separator2); + } + + return value; + } + + 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/Services/IContextMenuProvider.cs b/src/GuiApp/FileTime.Avalonia/Services/IContextMenuProvider.cs new file mode 100644 index 0000000..fb22430 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Services/IContextMenuProvider.cs @@ -0,0 +1,10 @@ +using FileTime.Core.Models; +using System.Collections.Generic; + +namespace FileTime.Avalonia.Services +{ + public interface IContextMenuProvider + { + List GetContextMenuForFolder(IContainer container); + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Services/ItemNameConverterService.cs b/src/GuiApp/FileTime.Avalonia/Services/ItemNameConverterService.cs index 954e402..f75135b 100644 --- a/src/GuiApp/FileTime.Avalonia/Services/ItemNameConverterService.cs +++ b/src/GuiApp/FileTime.Avalonia/Services/ItemNameConverterService.cs @@ -3,9 +3,7 @@ using FileTime.Avalonia.Application; using FileTime.Avalonia.Models; using FileTime.Avalonia.ViewModels; using MvvmGen; -using System; using System.Collections.Generic; -using System.Text; namespace FileTime.Avalonia.Services { @@ -35,8 +33,7 @@ namespace FileTime.Avalonia.Services nameParts.Add(new ItemNamePart(before)); } - //TODO: underline - nameParts.Add(new ItemNamePart(rapidTravel) { /*TextDecorations = new TextDecorationCollection() {new TextDecoration() }*/ }); + nameParts.Add(new ItemNamePart(rapidTravel) { TextDecorations = TextDecorations.Underline }); } if (nameLeft.Length > 0) diff --git a/src/GuiApp/FileTime.Avalonia/Services/WindowsContextMenuProvider.cs b/src/GuiApp/FileTime.Avalonia/Services/WindowsContextMenuProvider.cs new file mode 100644 index 0000000..b9f07dc --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Services/WindowsContextMenuProvider.cs @@ -0,0 +1,267 @@ +using System; +using Microsoft.Win32; +using Avalonia.Controls; +using FileTime.Core.Models; +using FileTime.Providers.Local; +using System.Runtime.InteropServices; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; +using System.IO; + +#pragma warning disable CA1416 +namespace FileTime.Avalonia.Services +{ + public class WindowsContextMenuProvider : IContextMenuProvider + { + public List GetContextMenuForFolder(IContainer container) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) throw new NotSupportedException(); + + var menuItems = new List(); + + if (container is LocalFolder localFolder) + { + ProcessKey(Registry.ClassesRoot.OpenSubKey("Directory"), menuItems, localFolder.Directory.FullName); + } + + return menuItems; + } + + private void ProcessKey(RegistryKey? contextMenuContainer, List menuItems, string folderPath) + { + var shell = contextMenuContainer?.OpenSubKey("shell"); + if (shell == null) return; + + var shellSubKeys = shell.GetSubKeyNames(); + + foreach (var shellKey in shellSubKeys.Select(k => shell.OpenSubKey(k)).OfType()) + { + var textBase = shellKey.GetValue(null) as string ?? shellKey.GetValue("MUIVerb") as string; + + if (textBase == null) continue; + + string? text = null; + if (textBase.StartsWith("@")) + { + var parts = textBase[1..].Split(','); + if (parts.Length == 2 && long.TryParse(parts[1], out var parsedResourceId)) + { + if (parsedResourceId < 0) parsedResourceId *= -1; + + text = GetStringResource(parts[0], (uint)parsedResourceId); + } + } + else + { + text = textBase; + } + + if (text != null) + { + text = text.Replace("&", ""); + + if (shellKey.GetSubKeyNames().Contains("command") && shellKey.OpenSubKey("command")?.GetValue(null) is string commandString) + { + var item = new MenuItem() { Header = text }; + item.Click += (o, e) => MenuItemClick(folderPath, commandString); + menuItems.Add(item); + } + else if (shellKey.GetValue("ExtendedSubCommandsKey") is string extendedCommands) + { + var rootMenu = new MenuItem() { Header = text }; + var rootMenuItems = new List(); + + ProcessKey(Registry.ClassesRoot.OpenSubKey(extendedCommands), rootMenuItems, folderPath); + + rootMenu.Items = rootMenuItems.ToArray(); + menuItems.Add(rootMenu); + } + } + } + } + + private static void MenuItemClick(string folderPath, string commandString) + { + var commandPartsWithoutAp = commandString.Split('\"').ToList(); + var commandParts = new List>(); + + for (var i = 0; i < commandPartsWithoutAp.Count; i++) + { + if (i % 2 == 0) + { + commandParts.Add(commandPartsWithoutAp[i].Split(' ').ToList()); + } + else + { + commandParts.Add(new List { commandPartsWithoutAp[i] }); + } + } + + for (var i = 0; i < commandParts.Count; i++) + { + for (var i2 = 0; i2 < commandParts[i].Count; i2++) + { + /*var commandPart = commandParts[i][i2]; + + if (commandPart == "%1" || commandPart == "%V") commandParts[i][i2] = folderPath;*/ + + commandParts[i][i2] = commandParts[i][i2].Replace("%1", folderPath).Replace("%V", folderPath); + } + } + + var commandPartsWithoutEmpty = commandParts.SelectMany(c => c).Where(c => !string.IsNullOrWhiteSpace(c)).ToList(); + + if (commandPartsWithoutEmpty.Count == 1) + { + Process.Start(commandPartsWithoutEmpty[0]); + } + else if (commandPartsWithoutEmpty.Count > 1) + { + var paramStartIndex1 = -1; + var paramStartIndex2 = -1; + var found = false; + + for (var x = 0; x < commandParts.Count && paramStartIndex1 == -1; x++) + { + for (var y = 0; y < commandParts[x].Count; y++) + { + if (found) + { + paramStartIndex1 = x; + paramStartIndex2 = y; + break; + } + + if (commandParts[x][y] == commandPartsWithoutEmpty[0]) + { + found = true; + } + } + } + var arguments = SumList(commandParts, paramStartIndex1, paramStartIndex2); + + try + { + var process = new Process(); + process.StartInfo.FileName = commandPartsWithoutEmpty[0]; + process.StartInfo.Arguments = arguments; + process.Start(); + } + catch + { + if (commandParts[0].Count > 0) + { + var executable = ""; + var lastExecutablePart = 0; + for (lastExecutablePart = 0; !File.Exists(executable) && lastExecutablePart < commandParts[0].Count; lastExecutablePart++) + { + executable += (lastExecutablePart == 0 ? "" : " ") + commandParts[0][lastExecutablePart]; + } + + lastExecutablePart--; + + if (File.Exists(executable)) + { + try + { + var (paramStartX, paramStartY) = GetCoordinatesFrom(commandParts, 1, 0, lastExecutablePart); + arguments = SumList(commandParts, paramStartX, paramStartY); + + var process = new Process(); + process.StartInfo.FileName = executable; + process.StartInfo.Arguments = arguments; + process.Start(); + } + catch + { + //TODO: error message + } + } + } + //TODO: ELSE error message + } + } + } + + private static string SumList(List> data, int paramStartIndex1, int paramStartIndex2) + { + var result = ""; + + for (var x = paramStartIndex1; x < data.Count; x++) + { + if (x % 2 == 1) result += "\""; + + result += string.Join( + ' ', + x == paramStartIndex1 + ? data[x].Skip(paramStartIndex2) + : data[x] + ); + + if (x % 2 == 1) result += "\""; + } + + return result; + } + + private static (int, int) GetCoordinatesFrom(List> data, int skip, int startX = 0, int startY = 0) + { + int skipping = 0; + var x = startX; + var y = startY; + + var resultX = -1; + var resultY = -1; + for (; x < data.Count && resultX == -1; x++, y = 0) + { + for (; y < data[x].Count; y++) + { + if (skipping == skip) + { + resultX = x; + resultY = y; + break; + } + + skipping++; + } + } + + 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 1137724..6df59fd 100644 --- a/src/GuiApp/FileTime.Avalonia/Startup.cs +++ b/src/GuiApp/FileTime.Avalonia/Startup.cs @@ -1,4 +1,5 @@ -using FileTime.Avalonia.Application; +using System.Runtime.InteropServices; +using FileTime.Avalonia.Application; using FileTime.Avalonia.Services; using FileTime.Avalonia.ViewModels; using Microsoft.Extensions.DependencyInjection; @@ -15,9 +16,20 @@ namespace FileTime.Avalonia } internal static IServiceCollection AddServices(this IServiceCollection serviceCollection) { - return serviceCollection + serviceCollection = serviceCollection .AddLogging() .AddSingleton(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + serviceCollection.AddSingleton(); + } + else + { + throw new System.Exception("TODO: implement linux contextmenu provider"); + } + + return serviceCollection; } } } diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs index 959d08f..362ddc8 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs @@ -51,9 +51,6 @@ namespace FileTime.Avalonia.ViewModels [Property] private List _rootDriveInfos; - [Property] - private ObservableCollection _tabs = new ObservableCollection(); - public Action? FocusDefaultElement { get; set; } async partial void OnInitialize() @@ -70,13 +67,9 @@ namespace FileTime.Avalonia.ViewModels await tab.Init(LocalContentProvider); var tabContainer = new TabContainer(tab, LocalContentProvider, ItemNameConverterService); - await tabContainer.Init(); - AppState.Tabs = new List() - { - tabContainer - }; - - _tabs.Add(new TabControlViewModel(1, tabContainer)); + await tabContainer.Init(1); + tabContainer.IsSelected = true; + AppState.Tabs.Add(tabContainer); var driveInfos = new List(); foreach (var drive in DriveInfo.GetDrives().Where(d => d.DriveType == DriveType.Fixed)) @@ -91,8 +84,7 @@ namespace FileTime.Avalonia.ViewModels } } - //TODO: order by - RootDriveInfos = driveInfos; + RootDriveInfos = driveInfos.OrderBy(d => d.Name).ToList(); } private async Task GetContainerForWindowsDrive(DriveInfo drive) @@ -203,30 +195,29 @@ namespace FileTime.Avalonia.ViewModels public async Task SwitchToTab(int number) { - var tab = _tabs.FirstOrDefault(t => t.TabNumber == number); + var tabContainer = AppState.Tabs.FirstOrDefault(t => t.TabNumber == number); if (number == -1) { - var greatestNumber = _tabs.Select(t => t.TabNumber).Max(); - tab = _tabs.FirstOrDefault(t => t.TabNumber == greatestNumber); + var greatestNumber = AppState.Tabs.Select(t => t.TabNumber).Max(); + tabContainer = AppState.Tabs.FirstOrDefault(t => t.TabNumber == greatestNumber); } - else if (tab == null) + else if (tabContainer == null) { var newContainer = await AppState.SelectedTab.CurrentLocation.Container.Clone(); var newTab = new Tab(); await newTab.Init(newContainer); - var tabContainer = new TabContainer(newTab, LocalContentProvider, ItemNameConverterService); - await tabContainer.Init(); + tabContainer = new TabContainer(newTab, LocalContentProvider, ItemNameConverterService); + await tabContainer.Init(number); - tab = new TabControlViewModel(number, tabContainer); var i = 0; - for (i = 0; i < Tabs.Count; i++) + for (i = 0; i < AppState.Tabs.Count; i++) { - if (Tabs[i].TabNumber > number) break; + if (AppState.Tabs[i].TabNumber > number) break; } - Tabs.Insert(i, tab); + AppState.Tabs.Insert(i, tabContainer); } if (AppState.ViewMode == ViewMode.RapidTravel) @@ -234,28 +225,29 @@ namespace FileTime.Avalonia.ViewModels await ExitRapidTravelMode(); } - AppState.SelectedTab = tab.Tab; + AppState.SelectedTab = tabContainer; - foreach (var tab2 in Tabs) + foreach (var tab2 in AppState.Tabs) { - tab2.IsSelected = tab2.TabNumber == tab.TabNumber; + tab2.IsSelected = tab2.TabNumber == tabContainer!.TabNumber; } } public async Task CloseTab() { - if (_tabs.Count > 1) + var tabs = AppState.Tabs; + if (tabs.Count > 1) { - var currentTab = _tabs.FirstOrDefault(t => t.Tab == AppState.SelectedTab); + var currentTab = tabs.FirstOrDefault(t => t == AppState.SelectedTab); if (currentTab != null) { - _tabs.Remove(currentTab); - var tabNumber = _tabs[0].TabNumber; - for (var i = 0; i < Tabs.Count; i++) + tabs.Remove(currentTab); + var tabNumber = tabs[0].TabNumber; + for (var i = 0; i < tabs.Count; i++) { - tabNumber = _tabs[i].TabNumber; - if (Tabs[i].TabNumber > currentTab.TabNumber) break; + tabNumber = tabs[i].TabNumber; + if (tabs[i].TabNumber > currentTab.TabNumber) break; } await SwitchToTab(tabNumber); } diff --git a/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml b/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml new file mode 100644 index 0000000..b2f6855 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml.cs b/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml.cs new file mode 100644 index 0000000..26b0fb8 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Views/ItemView.axaml.cs @@ -0,0 +1,27 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace FileTime.Avalonia.Views +{ + public partial class ItemView : UserControl + { + public static readonly StyledProperty ShowAttributesProperty = AvaloniaProperty.Register(nameof(ShowAttributes), true); + + public bool ShowAttributes + { + get => GetValue(ShowAttributesProperty); + set => SetValue(ShowAttributesProperty, value); + } + + public ItemView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml index d9ff6b0..2c98738 100644 --- a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml +++ b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml @@ -5,6 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:FileTime.Avalonia.ViewModels" + xmlns:local="using:FileTime.Avalonia.Views" Title="FileTime.Avalonia" d:DesignHeight="450" d:DesignWidth="800" @@ -88,19 +89,25 @@ - + - + - + Text="{Binding AppState.SelectedTab.TabNumber,StringFormat=({0})}" /> + + + + + + Items="{Binding AppState.Tabs}"> @@ -108,38 +115,17 @@ - - + + - - - + + + + + + + @@ -147,7 +133,7 @@ + Margin="20,0,0,0"> @@ -161,7 +147,13 @@ + SelectedItem="{Binding AppState.SelectedTab.CurrentLocation}"> + + + + + + + Classes="ContentListView"> + + + + + + Empty @@ -205,7 +204,13 @@ Classes="ContentListView" x:Name="ChildItems" Items="{Binding AppState.SelectedTab.ChildContainer.Items}" - IsVisible="{Binding AppState.SelectedTab.ChildContainer.Items.Count, Converter={StaticResource NotEqualsConverter}, ConverterParameter=0}" /> + IsVisible="{Binding AppState.SelectedTab.ChildContainer.Items.Count, Converter={StaticResource NotEqualsConverter}, ConverterParameter=0}"> + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GuiApp/FileTime.Avalonia/Views/PathPresenter.axaml.cs b/src/GuiApp/FileTime.Avalonia/Views/PathPresenter.axaml.cs new file mode 100644 index 0000000..85ba4c0 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Views/PathPresenter.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace FileTime.Avalonia.Views +{ + public partial class PathPresenter : UserControl + { + public PathPresenter() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Providers/FileTime.Providers.Local/LocalFile.cs b/src/Providers/FileTime.Providers.Local/LocalFile.cs index 69ded4b..22681ce 100644 --- a/src/Providers/FileTime.Providers.Local/LocalFile.cs +++ b/src/Providers/FileTime.Providers.Local/LocalFile.cs @@ -8,9 +8,7 @@ namespace FileTime.Providers.Local { public class LocalFile : IElement { - private readonly FileInfo _file; - - public FileInfo File => _file; + public FileInfo File { get; } public string Name { get; } @@ -18,26 +16,46 @@ namespace FileTime.Providers.Local public IContentProvider Provider { get; } - public bool IsHidden => (_file.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; + public bool IsHidden => (File.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; public bool IsSpecial => RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - && (new UnixFileInfo(_file.FullName).FileAccessPermissions & FileAccessPermissions.UserExecute) == FileAccessPermissions.UserExecute; + && (new UnixFileInfo(File.FullName).FileAccessPermissions & FileAccessPermissions.UserExecute) == FileAccessPermissions.UserExecute; + + public string Attributes => GetAttributes(); + + public DateTime CreatedAt => File.CreationTime; public LocalFile(FileInfo file, IContentProvider contentProvider) { - _file = file; + File = file; Name = file.Name; FullName = file.FullName; Provider = contentProvider; } - public string GetPrimaryAttributeText() => _file.Length.ToSizeString(); + public string GetPrimaryAttributeText() => File.Length.ToSizeString(); public Task Delete() { - _file.Delete(); + File.Delete(); return Task.CompletedTask; } + + public string GetAttributes() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return ""; + } + else + { + return "-" + + ((File.Attributes & FileAttributes.Archive) == FileAttributes.Archive ? "a" : "-") + + ((File.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly ? "r" : "-") + + ((File.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden ? "h" : "-") + + ((File.Attributes & FileAttributes.System) == FileAttributes.System ? "s" : "-"); + } + } } } \ 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 a94e937..fd5c446 100644 --- a/src/Providers/FileTime.Providers.Local/LocalFolder.cs +++ b/src/Providers/FileTime.Providers.Local/LocalFolder.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using AsyncEvent; using FileTime.Core.Models; using FileTime.Core.Providers; @@ -21,6 +22,9 @@ namespace FileTime.Providers.Local public string FullName { get; } public AsyncEventHandler Refreshed { get; } = new(); + public string Attributes => GetAttributes(); + + public DateTime CreatedAt => Directory.CreationTime; public LocalFolder(DirectoryInfo directory, LocalContentProvider contentProvider, IContainer? parent) { @@ -111,5 +115,21 @@ namespace FileTime.Providers.Local Directory.Delete(true); return Task.CompletedTask; } + + public string GetAttributes() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return ""; + } + else + { + return "d" + + ((Directory.Attributes & FileAttributes.Archive) == FileAttributes.Archive ? "a" : "-") + + ((Directory.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly ? "r" : "-") + + ((Directory.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden ? "h" : "-") + + ((Directory.Attributes & FileAttributes.System) == FileAttributes.System ? "s" : "-"); + } + } } } \ No newline at end of file