Refactor, Design, ContextMenu

This commit is contained in:
2022-01-27 10:33:47 +01:00
parent 0025339261
commit 80570d8895
18 changed files with 688 additions and 146 deletions

View File

@@ -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">
<Application.DataTemplates>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme Mode="Light"/>
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource ForegroundBrush}"/>
</Style>
<Style Selector="TextBlock.SmallText">
<Setter Property="FontSize" Value="12"/>
</Style>
<Style Selector="ListBox.ContentListView">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Grid ColumnDefinitions="Auto,*">
<Image
Grid.RowSpan="2"
Width="20"
Height="20"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Source="{SvgImage /Assets/material/folder.svg}" />
<ItemsControl
Grid.Column="1"
VerticalAlignment="Center"
Items="{Binding DisplayName}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock
Text="{Binding Text}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Styles>
<Application.DataTemplates>
</Application.DataTemplates>
<Application.Resources>
<ResourceDictionary>
@@ -64,6 +17,8 @@
<Color x:Key="SelectedItemBackgroundColor">#93a1a1</Color>
<Color x:Key="ForegroundColor">#93a1a1</Color>
<Color x:Key="AccentForegroundColor">#268bd2</Color>
<Color x:Key="LightForegroundColor">#7793a1a1</Color>
<Color x:Key="AlternativeForegroundColor">#93a1a1</Color>
<Color x:Key="SelectedItemForegroundColor">#073642</Color>
@@ -74,9 +29,9 @@
<GradientStop Offset="0.5" Color="#93a1a1" />
<GradientStop Offset="1" Color="#0093a1a1" />
</LinearGradientBrush>
<SolidColorBrush
x:Key="AppBackgroundBrush"
Color="{DynamicResource AppBackgroundColor}" />
@@ -97,6 +52,12 @@
<SolidColorBrush
x:Key="ForegroundBrush"
Color="{DynamicResource ForegroundColor}" />
<SolidColorBrush
x:Key="AccentForegroundBrush"
Color="{DynamicResource AccentForegroundColor}" />
<SolidColorBrush
x:Key="LightForegroundBrush"
Color="{DynamicResource LightForegroundColor}" />
<SolidColorBrush
x:Key="AlternativeForegroundBrush"
Color="{DynamicResource AlternativeForegroundColor}" />
@@ -108,11 +69,48 @@
x:Key="ErrorBrush"
Color="{DynamicResource ErrorColor}" />
<SolidColorBrush x:Key="SystemControlHighlightListAccentLowBrush" Color="{DynamicResource SelectedItemBackgroundColor}" />
<converters:FormatSizeConverter x:Key="FormatSizeConverter"/>
<converters:CompareConverter x:Key="EqualityConverter"/>
<converters:CompareConverter x:Key="NotEqualsConverter" ComparisonCondition="{x:Static converters:ComparisonCondition.NotEqual}"/>
<converters:SplitStringConverter x:Key="SplitStringConverter" />
<converters:ItemViewModeToBrushConverter
x:Key="ItemViewModeToForegroundConverter"
DefaultBrush="{StaticResource ForegroundBrush}"
AlternativeBrush="{StaticResource AlternativeForegroundBrush}"
SelectedBrush="{StaticResource SelectedItemForegroundBrush}"/>
<converters:ItemViewModeToBrushConverter
x:Key="ItemViewModeToBackgroundConverter"
DefaultBrush="{StaticResource ItemBackgroundBrush}"
AlternativeBrush="{StaticResource AlternativeItemBackgroundBrush}"
SelectedBrush="{StaticResource SelectedItemBackgroundBrush}"/>
<converters:ContextMenuGenerator x:Key="ContextMenuGenerator"/>
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<FluentTheme Mode="Light"/>
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource ForegroundBrush}"/>
<Setter Property="FontSize" Value="16"/>
</Style>
<Style Selector="TextBlock.SmallText">
<Setter Property="FontSize" Value="16"/>
</Style>
<Style Selector="ListBox.ContentListView">
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style Selector="ListBox.ContentListView > ListBoxItem">
<Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ContextMenu">
<ContextMenu Items="{Binding Converter={StaticResource ContextMenuGenerator}}"/>
</Setter>
</Style>
</Application.Styles>
</Application>

View File

@@ -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<TabContainer> _tabs = new List<TabContainer>();
private ObservableCollection<TabContainer> _tabs = new ObservableCollection<TabContainer>();
[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;
}
}
}

View File

@@ -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);

View File

@@ -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<IContextMenuProvider>() ?? 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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,10 @@
using FileTime.Core.Models;
using System.Collections.Generic;
namespace FileTime.Avalonia.Services
{
public interface IContextMenuProvider
{
List<object> GetContextMenuForFolder(IContainer container);
}
}

View File

@@ -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)

View File

@@ -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<object> GetContextMenuForFolder(IContainer container)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) throw new NotSupportedException();
var menuItems = new List<object>();
if (container is LocalFolder localFolder)
{
ProcessKey(Registry.ClassesRoot.OpenSubKey("Directory"), menuItems, localFolder.Directory.FullName);
}
return menuItems;
}
private void ProcessKey(RegistryKey? contextMenuContainer, List<object> 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<RegistryKey>())
{
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<object>();
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<List<string>>();
for (var i = 0; i < commandPartsWithoutAp.Count; i++)
{
if (i % 2 == 0)
{
commandParts.Add(commandPartsWithoutAp[i].Split(' ').ToList());
}
else
{
commandParts.Add(new List<string> { 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<List<string>> 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<List<string>> 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

View File

@@ -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<ItemNameConverterService>();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
serviceCollection.AddSingleton<IContextMenuProvider, WindowsContextMenuProvider>();
}
else
{
throw new System.Exception("TODO: implement linux contextmenu provider");
}
return serviceCollection;
}
}
}

View File

@@ -51,9 +51,6 @@ namespace FileTime.Avalonia.ViewModels
[Property]
private List<RootDriveInfo> _rootDriveInfos;
[Property]
private ObservableCollection<TabControlViewModel> _tabs = new ObservableCollection<TabControlViewModel>();
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>()
{
tabContainer
};
_tabs.Add(new TabControlViewModel(1, tabContainer));
await tabContainer.Init(1);
tabContainer.IsSelected = true;
AppState.Tabs.Add(tabContainer);
var driveInfos = new List<RootDriveInfo>();
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<IContainer> 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);
}

View File

@@ -0,0 +1,49 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="FileTime.Avalonia.Views.ItemView"
x:Name="ItemRoot" Background="{Binding ViewMode,Converter={StaticResource ItemViewModeToBackgroundConverter}}">
<Grid ColumnDefinitions="Auto,*,Auto" Margin="3">
<Grid.Styles>
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="{Binding DataContext.ViewMode,Converter={StaticResource ItemViewModeToForegroundConverter},ElementName=ItemRoot}"/>
</Style>
</Grid.Styles>
<Image
Grid.RowSpan="2"
Width="18"
Height="18"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Source="{SvgImage /Assets/material/folder.svg}" />
<ItemsControl
Grid.Column="1"
VerticalAlignment="Center"
Items="{Binding DisplayName}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock
Text="{Binding Text}"
TextDecorations="{Binding TextDecorations}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid Grid.Column="2" IsVisible="{Binding ShowAttributes,ElementName=ItemRoot}">
<Grid ColumnDefinitions="180,40">
<TextBlock Text="{Binding Item.CreatedAt}"/>
<TextBlock Grid.Column="1" Text="{Binding Item.Attributes}"/>
</Grid>
</Grid>
</Grid>
</UserControl>

View File

@@ -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<bool> ShowAttributesProperty = AvaloniaProperty.Register<ItemView, bool>(nameof(ShowAttributes), true);
public bool ShowAttributes
{
get => GetValue(ShowAttributesProperty);
set => SetValue(ShowAttributesProperty, value);
}
public ItemView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@@ -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 @@
</Border>
</Grid>
<Grid Grid.Column="1" RowDefinitions="Auto,*,Auto">
<Grid Grid.Column="1" RowDefinitions="40,*,Auto">
<Grid ColumnDefinitions="*,Auto">
<Grid>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="10,5"
Text="{Binding AppState.SelectedTab.CurrentLocation.Container.FullName}" />
</Grid>
Text="{Binding AppState.SelectedTab.TabNumber,StringFormat=({0})}" />
<local:PathPresenter Margin="10,5,0,5" DataContext="{Binding AppState.SelectedTab.CurrentLocation.Container.FullName}"/>
<TextBlock
Margin="0,5,10,5"
Text="{Binding AppState.SelectedTab.SelectedItem.Item.Name}" Foreground="{StaticResource AccentForegroundBrush}" />
</StackPanel>
<ItemsControl
Grid.Column="1"
HorizontalAlignment="Right"
Items="{Binding Tabs}">
Items="{Binding AppState.Tabs}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
@@ -108,38 +115,17 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal"
Margin="10,0,0,0">
<!--i:Interaction.Behaviors>
<ic:DataTriggerBehavior
Binding="{Binding IsSelected}"
Value="True">
<ic:ChangePropertyAction
PropertyName="Background"
Value="{DynamicResource ForegroundBrush}" />
<ic:ChangePropertyAction
PropertyName="Foreground"
TargetObject="{Binding ElementName=Text}"
Value="{DynamicResource ContainerBackgroundBrush}" />
</ic:DataTriggerBehavior>
<ic:DataTriggerBehavior
Binding="{Binding IsSelected}"
Value="False">
<ic:ChangePropertyAction
PropertyName="Background"
Value="{DynamicResource ContainerBackgroundBrush}" />
<ic:ChangePropertyAction
PropertyName="Foreground"
TargetObject="{Binding ElementName=Text}"
Value="{DynamicResource ForegroundBrush}" />
</ic:DataTriggerBehavior>
</i:Interaction.Behaviors-->
<Grid RowDefinitions="Auto,1">
<StackPanel Orientation="Horizontal" Margin="20,0,20,0">
<TextBlock
VerticalAlignment="Center" Text="{Binding TabNumber}" />
<TextBlock Margin="5,0,0,0"
VerticalAlignment="Center" Text="{Binding Tab.CurrentLocation.Container.FullName}" />
</StackPanel>
<TextBlock
VerticalAlignment="Center" Text="{Binding TabNumber,StringFormat=({0})}" />
<local:PathPresenter Margin="5,0,0,0" DataContext="{Binding CurrentLocation.Container.FullName}"/>
</StackPanel>
<Rectangle Fill="{DynamicResource ForegroundBrush}" Grid.Row="1" IsVisible="{Binding IsSelected}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
@@ -147,7 +133,7 @@
<Grid
Grid.Row="1"
Margin="20">
Margin="20,0,0,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="15*" />
@@ -161,7 +147,13 @@
<ListBox
Classes="ContentListView"
Items="{Binding AppState.SelectedTab.Parent.Items}"
SelectedItem="{Binding AppState.SelectedTab.CurrentLocation}" />
SelectedItem="{Binding AppState.SelectedTab.CurrentLocation}">
<ListBox.ItemTemplate>
<DataTemplate>
<local:ItemView ShowAttributes="False"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
<Rectangle
@@ -180,13 +172,20 @@
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
SelectedItem="{Binding AppState.SelectedTab.SelectedItem, Mode=TwoWay}"
Classes="ContentListView" />
Classes="ContentListView">
<ListBox.ItemTemplate>
<DataTemplate>
<local:ItemView/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock
x:Name="CurrentEmpty"
Margin="10"
HorizontalAlignment="Center"
FontWeight="Bold"
Foreground="{DynamicResource ErrorBrush}"
IsVisible="{Binding AppState.SelectedTab.CurrentLocation.Items.Count, Converter={StaticResource EqualityConverter}, ConverterParameter=0}">
Empty
</TextBlock>
@@ -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}">
<ListBox.ItemTemplate>
<DataTemplate>
<local:ItemView/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock
x:Name="ChildEmpty"

View File

@@ -0,0 +1,23 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:coremodels="using:FileTime.Core.Models"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="FileTime.Avalonia.Views.PathPresenter">
<ItemsControl Items="{Binding Converter={StaticResource SplitStringConverter}, ConverterParameter={x:Static coremodels:Constants.SeparatorChar}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}"/>
<TextBlock Text="/" Margin="5,0,5,0" Foreground="{DynamicResource LightForegroundBrush}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UserControl>

View File

@@ -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);
}
}
}

View File

@@ -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" : "-");
}
}
}
}

View File

@@ -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" : "-");
}
}
}
}