diff --git a/.vscode/launch.json b/.vscode/launch.json
index 3c6340a..dd6f1b1 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -9,9 +9,9 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build cli",
- "program": "${workspaceFolder}/src/ConsoleApp/FileTime.ConsoleUI/bin/Debug/net6.0/FileTime.ConsoleUI.dll",
+ "program": "${workspaceFolder}/src/ConsoleApp/FileTime.ConsoleUI.App/bin/Debug/net6.0/FileTime.ConsoleUI.App.dll",
"args": [],
- "cwd": "${workspaceFolder}/src/ConsoleApp/FileTime.ConsoleUI",
+ "cwd": "${workspaceFolder}/src/ConsoleApp/FileTime.ConsoleUI.App",
"console": "internalConsole",
"stopAtEntry": false
},
@@ -20,9 +20,9 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build gui",
- "program": "${workspaceFolder}/src/GuiApp/Avalonia/FileTime.GuiApp/bin/Debug/net6.0/FileTime.GuiApp.dll",
+ "program": "${workspaceFolder}/src/GuiApp/Avalonia/FileTime.GuiApp.App/bin/Debug/net6.0/FileTime.GuiApp.App.dll",
"args": [],
- "cwd": "${workspaceFolder}/src/GuiApp/Avalonia/FileTime.GuiApp/bin/Debug/net6.0",
+ "cwd": "${workspaceFolder}/src/GuiApp/Avalonia/FileTime.GuiApp.App/bin/Debug/net6.0",
"console": "internalConsole",
"stopAtEntry": false
},
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 746cb96..f95a2f1 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -7,7 +7,7 @@
"type": "process",
"args": [
"build",
- "${workspaceFolder}/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj",
+ "${workspaceFolder}/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
@@ -19,7 +19,7 @@
"type": "process",
"args": [
"build",
- "${workspaceFolder}/src/GuiApp/Avalonia/FileTime.GuiApp/FileTime.GuiApp.csproj",
+ "${workspaceFolder}/src/GuiApp/Avalonia/FileTime.GuiApp.App/FileTime.GuiApp.App.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
@@ -31,7 +31,7 @@
"type": "process",
"args": [
"publish",
- "${workspaceFolder}/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj",
+ "${workspaceFolder}/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
@@ -43,7 +43,7 @@
"type": "process",
"args": [
"publish",
- "${workspaceFolder}/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj",
+ "${workspaceFolder}/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj",
"-p:PublishSingleFile=true",
"-c",
"Release",
@@ -60,7 +60,7 @@
"type": "process",
"args": [
"publish",
- "${workspaceFolder}/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj",
+ "${workspaceFolder}/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj",
"-p:PublishSingleFile=true",
"-c",
"Release",
@@ -77,7 +77,7 @@
"type": "process",
"args": [
"publish",
- "${workspaceFolder}/src/GuiApp/Avalonia/FileTime.GuiApp/FileTime.GuiApp.csproj",
+ "${workspaceFolder}/src/GuiApp/Avalonia/FileTime.GuiApp.App/FileTime.GuiApp.App.csproj",
"-c",
"Release",
],
@@ -90,7 +90,7 @@
"args": [
"watch",
"run",
- "${workspaceFolder}/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj",
+ "${workspaceFolder}/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Command/Commands.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Command/Commands.cs
new file mode 100644
index 0000000..22a7765
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core.Abstraction/Command/Commands.cs
@@ -0,0 +1,66 @@
+namespace FileTime.App.Core.Command
+{
+ public enum Commands
+ {
+ None,
+
+ AutoRefresh,
+ ChangeTimelineMode,
+ CloseTab,
+ Compress,
+ Copy,
+ CopyHash,
+ CopyPath,
+ CreateContainer,
+ CreateElement,
+ Cut,
+ Edit,
+ EnterRapidTravel,
+ FindByName,
+ FindByNameRegex,
+ GoToHome,
+ GoToPath,
+ GoToProvider,
+ GoToRoot,
+ GoUp,
+ HardDelete,
+ Mark,
+ MoveCursorDown,
+ MoveCursorDownPage,
+ MoveCursorUp,
+ MoveCursorUpPage,
+ MoveToFirst,
+ MoveToLast,
+ NextTimelineBlock,
+ NextTimelineCommand,
+ Open,
+ OpenInFileBrowser,
+ OpenOrRun,
+ PasteMerge,
+ PasteOverwrite,
+ PasteSkip,
+ PinFavorite,
+ PreviousTimelineBlock,
+ PreviousTimelineCommand,
+ Refresh,
+ Rename,
+ RunCommand,
+ ScanContainerSize,
+ ShowAllShotcut,
+ SoftDelete,
+ SwitchToLastTab,
+ SwitchToTab1,
+ SwitchToTab2,
+ SwitchToTab3,
+ SwitchToTab4,
+ SwitchToTab5,
+ SwitchToTab6,
+ SwitchToTab7,
+ SwitchToTab8,
+ TimelinePause,
+ TimelineRefresh,
+ TimelineStart,
+ ToggleAdvancedIcons,
+ ToggleHidden,
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/FileTime.App.Core.Abstraction.csproj b/src/AppCommon/FileTime.App.Core.Abstraction/FileTime.App.Core.Abstraction.csproj
new file mode 100644
index 0000000..d2d0d89
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core.Abstraction/FileTime.App.Core.Abstraction.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/IAppState.cs b/src/AppCommon/FileTime.App.Core.Abstraction/IAppState.cs
new file mode 100644
index 0000000..60333d9
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core.Abstraction/IAppState.cs
@@ -0,0 +1,16 @@
+using System.Collections.ObjectModel;
+using FileTime.App.Core.ViewModels;
+
+namespace FileTime.App.Core
+{
+ public interface IAppState
+ {
+ ObservableCollection Tabs { get; }
+ ITabViewModel? SelectedTab { get; }
+ IObservable SearchText { get; }
+
+ void AddTab(ITabViewModel tabViewModel);
+ void RemoveTab(ITabViewModel tabViewModel);
+ void SetSearchText(string? searchText);
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Models/Enums/ItemAttributeType.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Models/Enums/ItemAttributeType.cs
new file mode 100644
index 0000000..030402b
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core.Abstraction/Models/Enums/ItemAttributeType.cs
@@ -0,0 +1,10 @@
+namespace FileTime.App.Core.Models.Enums
+{
+ public enum ItemAttributeType
+ {
+ File,
+ Element,
+ Container,
+ SizeContainer
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Models/Enums/ItemViewMode.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Models/Enums/ItemViewMode.cs
new file mode 100644
index 0000000..2f8eb6e
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core.Abstraction/Models/Enums/ItemViewMode.cs
@@ -0,0 +1,12 @@
+namespace FileTime.App.Core.Models.Enums
+{
+ public enum ItemViewMode
+ {
+ Default,
+ Alternative,
+ Selected,
+ Marked,
+ MarkedSelected,
+ MarkedAlternative
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Models/ItemNamePart.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Models/ItemNamePart.cs
new file mode 100644
index 0000000..81e01fe
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core.Abstraction/Models/ItemNamePart.cs
@@ -0,0 +1,14 @@
+namespace FileTime.App.Core.Models
+{
+ public class ItemNamePart
+ {
+ public string Text { get; set; }
+ public bool IsSpecial { get; set; }
+
+ public ItemNamePart(string text, bool isSpecial = false)
+ {
+ Text = text;
+ IsSpecial = isSpecial;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Services/IItemNameConverterService.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IItemNameConverterService.cs
new file mode 100644
index 0000000..72825c1
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IItemNameConverterService.cs
@@ -0,0 +1,11 @@
+using FileTime.App.Core.Models;
+
+namespace FileTime.App.Core.Services
+{
+ public interface IItemNameConverterService
+ {
+ List GetDisplayName(string name, string? searchText);
+ string GetFileExtension(string fullName);
+ string GetFileName(string fullName);
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerSizeContainerViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerSizeContainerViewModel.cs
new file mode 100644
index 0000000..b405f2d
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerSizeContainerViewModel.cs
@@ -0,0 +1,7 @@
+namespace FileTime.App.Core.ViewModels
+{
+ public interface IContainerSizeContainerViewModel : IItemViewModel
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerViewModel.cs
new file mode 100644
index 0000000..26bb120
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IContainerViewModel.cs
@@ -0,0 +1,7 @@
+namespace FileTime.App.Core.ViewModels
+{
+ public interface IContainerViewModel : IItemViewModel
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IElementViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IElementViewModel.cs
new file mode 100644
index 0000000..b3ea19a
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IElementViewModel.cs
@@ -0,0 +1,7 @@
+namespace FileTime.App.Core.ViewModels
+{
+ public interface IElementViewModel : IItemViewModel
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IFileViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IFileViewModel.cs
new file mode 100644
index 0000000..d6bd017
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IFileViewModel.cs
@@ -0,0 +1,7 @@
+namespace FileTime.App.Core.ViewModels
+{
+ public interface IFileViewModel : IElementViewModel
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IItemViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IItemViewModel.cs
new file mode 100644
index 0000000..f299ca5
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IItemViewModel.cs
@@ -0,0 +1,15 @@
+using FileTime.App.Core.Models;
+using FileTime.App.Core.Models.Enums;
+using FileTime.Core.Models;
+
+namespace FileTime.App.Core.ViewModels
+{
+ public interface IItemViewModel
+ {
+ IItem? Item { get; set; }
+ IObservable>? DisplayName { get; set; }
+ IObservable? IsSelected { get; set; }
+ IObservable? IsMarked { get; set; }
+ ItemViewMode ViewMode { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs
new file mode 100644
index 0000000..ed528e0
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/ITabViewModel.cs
@@ -0,0 +1,16 @@
+
+using FileTime.Core.Models;
+using FileTime.Core.Services;
+using InitableService;
+
+namespace FileTime.App.Core.ViewModels
+{
+ public interface ITabViewModel : IInitable
+ {
+ IObservable? CurrentLocation { get; }
+ IObservable? CurrentSelectedItem { get; }
+ IObservable>? CurrentItems { get; }
+ IObservable> MarkedItems { get; }
+ ITab? Tab { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core/AppStateBase.cs b/src/AppCommon/FileTime.App.Core/AppStateBase.cs
new file mode 100644
index 0000000..6ec81aa
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core/AppStateBase.cs
@@ -0,0 +1,49 @@
+using System.Collections.ObjectModel;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using FileTime.App.Core.ViewModels;
+using MvvmGen;
+
+namespace FileTime.App.Core
+{
+ [ViewModel]
+ public abstract partial class AppStateBase : IAppState
+ {
+ private readonly BehaviorSubject _searchText = new(null);
+ public ObservableCollection Tabs { get; } = new();
+ public IObservable SearchText { get; private set; }
+
+ [Property]
+ private ITabViewModel? _selectedTab;
+
+ partial void OnInitialize()
+ {
+ SearchText = _searchText.AsObservable();
+ }
+
+ public void AddTab(ITabViewModel tabViewModel)
+ {
+ Tabs.Add(tabViewModel);
+ if (_selectedTab == null)
+ {
+ SelectedTab = Tabs.First();
+ }
+ }
+
+ public void RemoveTab(ITabViewModel tabViewModel)
+ {
+ if (!Tabs.Contains(tabViewModel)) return;
+
+ Tabs.Remove(tabViewModel);
+ if (_selectedTab == tabViewModel)
+ {
+ SelectedTab = Tabs.FirstOrDefault();
+ }
+ }
+
+ public void SetSearchText(string? searchText)
+ {
+ _searchText.OnNext(searchText);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core/FileTime.App.Core.csproj b/src/AppCommon/FileTime.App.Core/FileTime.App.Core.csproj
new file mode 100644
index 0000000..bd8eec7
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core/FileTime.App.Core.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AppCommon/FileTime.App.Core/Services/ItemNameConverterService.cs b/src/AppCommon/FileTime.App.Core/Services/ItemNameConverterService.cs
new file mode 100644
index 0000000..24f85ae
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core/Services/ItemNameConverterService.cs
@@ -0,0 +1,56 @@
+using FileTime.App.Core.Models;
+
+namespace FileTime.App.Core.Services
+{
+ public class ItemNameConverterService : IItemNameConverterService
+ {
+ public List GetDisplayName(string name, string? searchText)
+ {
+ var nameParts = new List();
+ searchText = searchText?.ToLower();
+
+ if (!string.IsNullOrEmpty(searchText))
+ {
+ var nameLeft = name;
+
+ while (nameLeft.ToLower().IndexOf(searchText, StringComparison.Ordinal) is int rapidTextStart && rapidTextStart != -1)
+ {
+ var before = rapidTextStart > 0 ? nameLeft.Substring(0, rapidTextStart) : null;
+ var rapidTravel = nameLeft.Substring(rapidTextStart, searchText.Length);
+
+ nameLeft = nameLeft.Substring(rapidTextStart + searchText.Length);
+
+ if (before != null)
+ {
+ nameParts.Add(new ItemNamePart(before));
+ }
+
+ nameParts.Add(new ItemNamePart(rapidTravel, true));
+ }
+
+ if (nameLeft.Length > 0)
+ {
+ nameParts.Add(new ItemNamePart(nameLeft));
+ }
+ }
+ else
+ {
+ nameParts.Add(new ItemNamePart(name));
+ }
+ return nameParts;
+ }
+
+ public string GetFileName(string fullName)
+ {
+ var parts = fullName.Split('.');
+ var fileName = string.Join('.', parts[..^1]);
+ return string.IsNullOrEmpty(fileName) ? fullName : fileName;
+ }
+
+ public string GetFileExtension(string fullName)
+ {
+ var parts = fullName.Split('.');
+ return parts.Length == 1 || (parts.Length == 2 && string.IsNullOrEmpty(parts[0])) ? "" : parts[^1];
+ }
+ }
+}
diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ContainerSizeContainerViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ContainerSizeContainerViewModel.cs
new file mode 100644
index 0000000..24b22fa
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core/ViewModels/ContainerSizeContainerViewModel.cs
@@ -0,0 +1,7 @@
+namespace FileTime.App.Core.ViewModels
+{
+ public class ContainerSizeContainerViewModel : ItemViewModel, IContainerSizeContainerViewModel
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ContainerViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ContainerViewModel.cs
new file mode 100644
index 0000000..9b3de7e
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core/ViewModels/ContainerViewModel.cs
@@ -0,0 +1,6 @@
+namespace FileTime.App.Core.ViewModels
+{
+ public class ContainerViewModel : ItemViewModel, IContainerViewModel
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs
new file mode 100644
index 0000000..5aa99f2
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs
@@ -0,0 +1,7 @@
+namespace FileTime.App.Core.ViewModels
+{
+ public class ElementViewModel : ItemViewModel, IElementViewModel
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/FileViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/FileViewModel.cs
new file mode 100644
index 0000000..9cd8407
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core/ViewModels/FileViewModel.cs
@@ -0,0 +1,7 @@
+namespace FileTime.App.Core.ViewModels
+{
+ public class FileViewModel : ElementViewModel, IFileViewModel
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs
new file mode 100644
index 0000000..4b8f0c0
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs
@@ -0,0 +1,26 @@
+using FileTime.App.Core.Models;
+using FileTime.App.Core.Models.Enums;
+using FileTime.Core.Models;
+using MvvmGen;
+
+namespace FileTime.App.Core.ViewModels
+{
+ [ViewModel]
+ public abstract partial class ItemViewModel : IItemViewModel
+ {
+ [Property]
+ private IItem? _item;
+
+ [Property]
+ private IObservable>? _displayName;
+
+ [Property]
+ private IObservable? _isSelected;
+
+ [Property]
+ private IObservable? _isMarked;
+
+ [Property]
+ private ItemViewMode _viewMode;
+ }
+}
\ No newline at end of file
diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs
new file mode 100644
index 0000000..cfe3911
--- /dev/null
+++ b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs
@@ -0,0 +1,105 @@
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using FileTime.App.Core.Services;
+using FileTime.Core.Models;
+using FileTime.Core.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace FileTime.App.Core.ViewModels
+{
+ public class TabViewModel : ITabViewModel, IDisposable
+ {
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IItemNameConverterService _itemNameConverterService;
+ private readonly IAppState _appState;
+ private readonly BehaviorSubject> _markedItems = new(Enumerable.Empty());
+ private readonly List _disposables = new();
+ private bool disposed;
+
+ public IObservable? CurrentLocation { get; private set; }
+ public IObservable? CurrentSelectedItem { get; private set; }
+ public IObservable>? CurrentItems { get; private set; }
+ public IObservable> MarkedItems { get; }
+
+ public ITab? Tab { get; private set; }
+
+ public TabViewModel(
+ IServiceProvider serviceProvider,
+ IItemNameConverterService itemNameConverterService,
+ IAppState appState)
+ {
+ _serviceProvider = serviceProvider;
+ _itemNameConverterService = itemNameConverterService;
+ _appState = appState;
+
+ MarkedItems = _markedItems.Select(e => e.ToList()).AsObservable();
+ }
+
+ public void Init(ITab tab)
+ {
+ CurrentLocation = tab.CurrentLocation.AsObservable();
+ CurrentItems = tab.CurrentItems.Select(items => items.Select(MapItemToViewModel).ToList());
+ CurrentSelectedItem = CurrentItems.CombineLatest(
+ tab.CurrentSelectedItem,
+ (currentItems, currentSelectedItemPath) => currentItems.FirstOrDefault(i => i.Item?.FullName == currentSelectedItemPath?.Path));
+ tab.CurrentLocation.Subscribe((_) => _markedItems.OnNext(Enumerable.Empty()));
+
+ Tab = tab;
+ }
+
+ private IItemViewModel MapItemToViewModel(IItem item)
+ {
+ if (item is IContainer container)
+ {
+ var containerViewModel = _serviceProvider.GetRequiredService();
+ InitIItemViewModel(containerViewModel, item);
+
+ return containerViewModel;
+ }
+ else if (item is IElement element)
+ {
+ var elementViewModel = _serviceProvider.GetRequiredService();
+ InitIItemViewModel(elementViewModel, item);
+
+ return elementViewModel;
+ }
+
+ throw new ArgumentException($"{nameof(item)} is not {nameof(IContainer)} neighter {nameof(IElement)}");
+
+ void InitIItemViewModel(IItemViewModel itemViewModel, IItem item)
+ {
+ itemViewModel.Item = item;
+ itemViewModel.DisplayName = _appState.SearchText.Select(s => _itemNameConverterService.GetDisplayName(item.DisplayName, s));
+ itemViewModel.IsMarked = MarkedItems.Select(m => m.Contains(item.FullName));
+ itemViewModel.IsSelected = MarkedItems.Select(m => m.Contains(item.FullName));
+ }
+ }
+
+ ~TabViewModel()
+ {
+ Dispose(false);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!disposed && disposing)
+ {
+ foreach (var disposable in _disposables)
+ {
+ try
+ {
+ disposable.Dispose();
+ }
+ catch { }
+ }
+ }
+ disposed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Core/FileTime.Core.Abstraction/FileTime.Core.Abstraction.csproj b/src/Core/FileTime.Core.Abstraction/FileTime.Core.Abstraction.csproj
index f2d4187..3dc96ef 100644
--- a/src/Core/FileTime.Core.Abstraction/FileTime.Core.Abstraction.csproj
+++ b/src/Core/FileTime.Core.Abstraction/FileTime.Core.Abstraction.csproj
@@ -10,4 +10,8 @@
+
+
+
+
diff --git a/src/Core/FileTime.Core.Abstraction/Models/Constants.cs b/src/Core/FileTime.Core.Abstraction/Models/Constants.cs
index 7244ea6..c414f03 100644
--- a/src/Core/FileTime.Core.Abstraction/Models/Constants.cs
+++ b/src/Core/FileTime.Core.Abstraction/Models/Constants.cs
@@ -3,5 +3,6 @@ namespace FileTime.Core.Models
public static class Constants
{
public const char SeparatorChar = '/';
+ public const int MaximumObservableMergeOperations = 4;
}
}
\ No newline at end of file
diff --git a/src/Core/FileTime.Core.Abstraction/Services/ITab.cs b/src/Core/FileTime.Core.Abstraction/Services/ITab.cs
index 7420885..9c2949e 100644
--- a/src/Core/FileTime.Core.Abstraction/Services/ITab.cs
+++ b/src/Core/FileTime.Core.Abstraction/Services/ITab.cs
@@ -1,7 +1,14 @@
+using FileTime.Core.Models;
+using InitableService;
+
namespace FileTime.Core.Services
{
- public interface ITab
+ public interface ITab : IInitable
{
-
+ IObservable CurrentLocation { get; }
+ IObservable CurrentSelectedItem { get; }
+ IObservable> CurrentItems { get; }
+
+ void ChangeLocation(IContainer newLocation);
}
}
\ No newline at end of file
diff --git a/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj b/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj
index 13eed31..ac049af 100644
--- a/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj
+++ b/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj
@@ -7,6 +7,7 @@
+
diff --git a/src/Core/FileTime.Core.Services/Tab.cs b/src/Core/FileTime.Core.Services/Tab.cs
index 78913ad..7877f72 100644
--- a/src/Core/FileTime.Core.Services/Tab.cs
+++ b/src/Core/FileTime.Core.Services/Tab.cs
@@ -1,7 +1,65 @@
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using FileTime.Core.Models;
+
namespace FileTime.Core.Services
{
public class Tab : ITab
{
-
+ private readonly BehaviorSubject _currentLocation = new(null);
+ private readonly BehaviorSubject _currentSelectedItem = new(null);
+ public IObservable CurrentLocation { get; }
+ public IObservable> CurrentItems { get; }
+ public IObservable CurrentSelectedItem { get; }
+
+ public Tab()
+ {
+ CurrentLocation = _currentLocation.AsObservable();
+ CurrentItems = _currentLocation
+ .Select(c =>
+ Observable.FromAsync(async () =>
+ c == null
+ ? Enumerable.Empty()
+ : await c.Items
+ .ToAsyncEnumerable()
+ .SelectAwait(
+ async i =>
+ {
+ try
+ {
+ //TODO: force create by AbsolutePath name
+ return await i.ContentProvider.GetItemByFullNameAsync(i.Path);
+ }
+ catch { return null!; }
+ }
+
+ )
+ .Where(i => i != null)
+ .ToListAsync()
+ )
+ )
+ .Merge(Constants.MaximumObservableMergeOperations);
+ CurrentSelectedItem = CurrentLocation.Select(GetSelectedItemByLocation).Merge(_currentSelectedItem).Throttle(TimeSpan.FromMilliseconds(500));
+
+ CurrentItems.Subscribe(c =>
+ {
+ ;
+ });
+ }
+
+ public void Init(IContainer currentLocation)
+ {
+ _currentLocation.OnNext(currentLocation);
+ }
+
+ private IAbsolutePath? GetSelectedItemByLocation(IContainer? currentLocation)
+ {
+ return currentLocation?.Items[0];
+ }
+
+ public void ChangeLocation(IContainer newLocation)
+ {
+ _currentLocation.OnNext(newLocation);
+ }
}
}
\ No newline at end of file
diff --git a/src/FileTime.sln b/src/FileTime.sln
index 939bc35..a86ec00 100644
--- a/src/FileTime.sln
+++ b/src/FileTime.sln
@@ -19,7 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AppCommon", "AppCommon", "{
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Avalonia", "Avalonia", "{01F231DE-4A65-435F-B4BB-77EE5221890C}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.GuiApp", "GuiApp\Avalonia\FileTime.GuiApp\FileTime.GuiApp.csproj", "{C389087E-EB78-4DCD-96AF-F1E2A4DEE0B0}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.GuiApp.App", "GuiApp\Avalonia\FileTime.GuiApp.App\FileTime.GuiApp.App.csproj", "{C389087E-EB78-4DCD-96AF-F1E2A4DEE0B0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{2FC40FE1-4446-44AB-BF77-00F94D995FA3}"
EndProject
@@ -31,6 +31,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.ConsoleUI", "Conso
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.ConsoleUI.App", "ConsoleApp\FileTime.ConsoleUI.App\FileTime.ConsoleUI.App.csproj", "{77ABB06A-43AF-49D6-A802-A389CCB53724}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Library", "Library", "{07CA18AA-B85D-4DEE-BB86-F569F6029853}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InitableService", "Library\InitableService\InitableService.csproj", "{C67E3453-9730-40B5-8D2D-5D4EEBE008E2}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.App.Core", "AppCommon\FileTime.App.Core\FileTime.App.Core.csproj", "{FC4AE72E-E302-45A0-BB9D-E87BB3250F0D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.App.Core.Abstraction", "AppCommon\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj", "{3E9CCD7F-86AA-4BD8-A100-0CD71BDB1AA0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.GuiApp", "GuiApp\Avalonia\FileTime.GuiApp\FileTime.GuiApp.csproj", "{26331AB9-6E4D-40DB-8FF0-CB7133F67CA0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.GuiApp.CustomImpl", "GuiApp\Avalonia\FileTime.GuiApp.CustomImpl\FileTime.GuiApp.CustomImpl.csproj", "{4B742649-225F-4C73-B118-1B29FE2A5774}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileTime.Providers.Local.Abstractions", "Providers\FileTime.Providers.Local.Abstractions\FileTime.Providers.Local.Abstractions.csproj", "{1500A537-2116-4111-B216-7632040619B0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -69,6 +83,30 @@ Global
{77ABB06A-43AF-49D6-A802-A389CCB53724}.Debug|Any CPU.Build.0 = Debug|Any CPU
{77ABB06A-43AF-49D6-A802-A389CCB53724}.Release|Any CPU.ActiveCfg = Release|Any CPU
{77ABB06A-43AF-49D6-A802-A389CCB53724}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C67E3453-9730-40B5-8D2D-5D4EEBE008E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C67E3453-9730-40B5-8D2D-5D4EEBE008E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C67E3453-9730-40B5-8D2D-5D4EEBE008E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C67E3453-9730-40B5-8D2D-5D4EEBE008E2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FC4AE72E-E302-45A0-BB9D-E87BB3250F0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FC4AE72E-E302-45A0-BB9D-E87BB3250F0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FC4AE72E-E302-45A0-BB9D-E87BB3250F0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FC4AE72E-E302-45A0-BB9D-E87BB3250F0D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3E9CCD7F-86AA-4BD8-A100-0CD71BDB1AA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3E9CCD7F-86AA-4BD8-A100-0CD71BDB1AA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3E9CCD7F-86AA-4BD8-A100-0CD71BDB1AA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3E9CCD7F-86AA-4BD8-A100-0CD71BDB1AA0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {26331AB9-6E4D-40DB-8FF0-CB7133F67CA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {26331AB9-6E4D-40DB-8FF0-CB7133F67CA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {26331AB9-6E4D-40DB-8FF0-CB7133F67CA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {26331AB9-6E4D-40DB-8FF0-CB7133F67CA0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4B742649-225F-4C73-B118-1B29FE2A5774}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4B742649-225F-4C73-B118-1B29FE2A5774}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4B742649-225F-4C73-B118-1B29FE2A5774}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4B742649-225F-4C73-B118-1B29FE2A5774}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1500A537-2116-4111-B216-7632040619B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1500A537-2116-4111-B216-7632040619B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1500A537-2116-4111-B216-7632040619B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1500A537-2116-4111-B216-7632040619B0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -83,6 +121,12 @@ Global
{A55F4A86-03ED-4D25-807A-04619A1D9507} = {A5291117-3001-498B-AC8B-E14F71F72570}
{67D4102C-8B92-451C-B4A9-5A82B68A7A5D} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549}
{77ABB06A-43AF-49D6-A802-A389CCB53724} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549}
+ {C67E3453-9730-40B5-8D2D-5D4EEBE008E2} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
+ {FC4AE72E-E302-45A0-BB9D-E87BB3250F0D} = {A5291117-3001-498B-AC8B-E14F71F72570}
+ {3E9CCD7F-86AA-4BD8-A100-0CD71BDB1AA0} = {A5291117-3001-498B-AC8B-E14F71F72570}
+ {26331AB9-6E4D-40DB-8FF0-CB7133F67CA0} = {01F231DE-4A65-435F-B4BB-77EE5221890C}
+ {4B742649-225F-4C73-B118-1B29FE2A5774} = {01F231DE-4A65-435F-B4BB-77EE5221890C}
+ {1500A537-2116-4111-B216-7632040619B0} = {2FC40FE1-4446-44AB-BF77-00F94D995FA3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml
new file mode 100644
index 0000000..6d07da0
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml
@@ -0,0 +1,215 @@
+
+
+
+ #E7073642
+ #083e4c
+ #D0083e4c
+ #80083e4c
+
+ #00000000
+ #10000000
+ #93a1a1
+ #00000000
+ #10000000
+ #b58900
+
+ #93a1a1
+ #268bd2
+ #fdf6e3
+ #7793a1a1
+ #93a1a1
+ #073642
+ #b58900
+ #b58900
+ #002b36
+
+ #dc322f
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/App.axaml.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml.cs
similarity index 51%
rename from src/GuiApp/Avalonia/FileTime.GuiApp/App.axaml.cs
rename to src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml.cs
index 7a7883f..3782f81 100644
--- a/src/GuiApp/Avalonia/FileTime.GuiApp/App.axaml.cs
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/App.axaml.cs
@@ -1,13 +1,29 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
+using FileTime.App.DependencyInjection;
using FileTime.GuiApp.ViewModels;
using FileTime.GuiApp.Views;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
namespace FileTime.GuiApp
{
public partial class App : Application
{
+ static App()
+ {
+ DI.ServiceProvider ??= DependencyInjection
+ .RegisterDefaultServices()
+ .AddConfiguration()
+ .RegisterLogging()
+ .AddViewModels()
+ .BuildServiceProvider()
+ .InitSerilog();
+
+ var logger = DI.ServiceProvider.GetRequiredService>();
+ logger.LogInformation("App initialization completed.");
+ }
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
@@ -19,7 +35,7 @@ namespace FileTime.GuiApp
{
desktop.MainWindow = new MainWindow
{
- DataContext = new MainWindowViewModel(),
+ DataContext = new MainWindowLoadingViewModel(),
};
}
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/FileTime.GuiApp.App.csproj b/src/GuiApp/Avalonia/FileTime.GuiApp.App/FileTime.GuiApp.App.csproj
new file mode 100644
index 0000000..5c260d9
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/FileTime.GuiApp.App.csproj
@@ -0,0 +1,44 @@
+
+
+ WinExe
+ net6.0
+ enable
+
+ copyused
+ true
+ filetime.ico
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Program.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Program.cs
new file mode 100644
index 0000000..d170055
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Program.cs
@@ -0,0 +1,62 @@
+using System;
+using System.IO;
+using Avalonia;
+using FileTime.GuiApp;
+
+namespace FileTime.GuiApp
+{
+ public static class Program
+ {
+ public static string AppDataRoot { get; }
+ public static string EnvironmentName { get; }
+
+ static Program()
+ {
+
+#if DEBUG
+ EnvironmentName = "Development";
+
+ AppDataRoot = Path.Combine(Environment.CurrentDirectory, "appdata");
+#else
+ EnvironmentName = "Release";
+
+ var possibleDataRootsPaths = new List()
+ {
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FileTime"),
+ Path.Combine(Assembly.GetEntryAssembly()?.Location ?? ".", "fallbackDataRoot")
+ };
+
+ string? appDataRoot = null;
+ foreach (var possibleAppDataRoot in possibleDataRootsPaths)
+ {
+ try
+ {
+ var appDataRootDirectory = new DirectoryInfo(possibleAppDataRoot);
+ if (!appDataRootDirectory.Exists) appDataRootDirectory.Create();
+
+ //TODO write test
+ appDataRoot = possibleAppDataRoot;
+ break;
+ }
+ catch { }
+ }
+
+ if (appDataRoot == null) throw new UnauthorizedAccessException();
+ AppDataRoot = appDataRoot;
+#endif
+ }
+
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .LogToTrace();
+ }
+}
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs
new file mode 100644
index 0000000..6ae33b2
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Startup.cs
@@ -0,0 +1,77 @@
+using System;
+using System.IO;
+using FileTime.App.Core;
+using FileTime.App.Core.Services;
+using FileTime.App.Core.ViewModels;
+using FileTime.Core.Services;
+using FileTime.GuiApp.Configuration;
+using FileTime.GuiApp.Logging;
+using FileTime.GuiApp.ViewModels;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Serilog;
+using Serilog.Configuration;
+
+namespace FileTime.GuiApp
+{
+ public static class Startup
+ {
+ internal static IServiceCollection AddViewModels(this IServiceCollection serviceCollection)
+ {
+ return serviceCollection
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton(s => s.GetRequiredService())
+ //TODO: move??
+ .AddTransient()
+ .AddTransient()
+ .AddTransient()
+ .AddTransient()
+ .AddTransient();
+ }
+
+ internal static IServiceCollection RegisterLogging(this IServiceCollection serviceCollection)
+ {
+ return serviceCollection.AddLogging(loggingBuilder =>
+ loggingBuilder.AddSerilog(dispose: true)
+ );
+ }
+
+ internal static IServiceCollection AddConfiguration(this IServiceCollection serviceCollection)
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(MainConfiguration.Configuration)
+ .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
+ .AddJsonFile($"appsettings.{Program.EnvironmentName}.json", true, true)
+ .Build();
+
+ return serviceCollection
+ .Configure(configuration.GetSection(SectionNames.ProgramsSectionName))
+ .Configure(configuration.GetSection(SectionNames.KeybindingSectionName))
+ .AddSingleton(configuration);
+ }
+
+ internal static IServiceProvider InitSerilog(this IServiceProvider serviceProvider)
+ {
+ Log.Logger = new LoggerConfiguration()
+ .ReadFrom.Configuration(serviceProvider.GetService())
+ .Enrich.FromLogContext()
+ .WriteTo.File(
+ Path.Combine(Program.AppDataRoot, "logs", "appLog.log"),
+ fileSizeLimitBytes: 10 * 1024 * 1024,
+ rollOnFileSizeLimit: true,
+ rollingInterval: RollingInterval.Day)
+ .WriteTo.MessageBoxSink(serviceProvider)
+ .CreateLogger();
+
+ return serviceProvider;
+ }
+
+ internal static LoggerConfiguration MessageBoxSink(
+ this LoggerSinkConfiguration loggerConfiguration,
+ IServiceProvider serviceProvider)
+ {
+ return loggerConfiguration.Sink(serviceProvider.GetService());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/appsettings.Development.json b/src/GuiApp/Avalonia/FileTime.GuiApp.App/appsettings.Development.json
new file mode 100644
index 0000000..bfd4587
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/appsettings.Development.json
@@ -0,0 +1,11 @@
+{
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Debug",
+ "Override": {
+ "Microsoft": "Information",
+ "System": "Warning"
+ }
+ }
+ }
+}
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/filetime.ico b/src/GuiApp/Avalonia/FileTime.GuiApp.App/filetime.ico
new file mode 100644
index 0000000..217f28f
Binary files /dev/null and b/src/GuiApp/Avalonia/FileTime.GuiApp.App/filetime.ico differ
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/FileTime.GuiApp.CustomImpl.csproj b/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/FileTime.GuiApp.CustomImpl.csproj
new file mode 100644
index 0000000..4f17a4f
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/FileTime.GuiApp.CustomImpl.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/ViewModels/AppState.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/ViewModels/AppState.cs
new file mode 100644
index 0000000..8295111
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp.CustomImpl/ViewModels/AppState.cs
@@ -0,0 +1,6 @@
+using FileTime.App.Core;
+
+namespace FileTime.GuiApp.ViewModels
+{
+ public class AppState : AppStateBase { }
+}
\ No newline at end of file
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/App.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/App.axaml
deleted file mode 100644
index a325e5c..0000000
--- a/src/GuiApp/Avalonia/FileTime.GuiApp/App.axaml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/CommandBindingConfiguration.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/CommandBindingConfiguration.cs
new file mode 100644
index 0000000..d1b1472
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/CommandBindingConfiguration.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Input;
+using FileTime.App.Core.Command;
+
+namespace FileTime.GuiApp.Configuration
+{
+ public class CommandBindingConfiguration
+ {
+ public List Keys { get; set; } = new List();
+
+ public Commands Command { get; set; } = Commands.None;
+
+ public string KeysDisplayText => GetKeysDisplayText();
+
+ public CommandBindingConfiguration() { }
+
+ public CommandBindingConfiguration(Commands command, IEnumerable keys)
+ {
+ Keys = new List(keys);
+ Command = command;
+ }
+
+ public CommandBindingConfiguration(Commands command, KeyConfig key)
+ {
+ Keys = new List() { key };
+ Command = command;
+ }
+
+ public CommandBindingConfiguration(Commands command, IEnumerable keys)
+ {
+ Keys = keys.Select(k => new KeyConfig(k)).ToList();
+ Command = command;
+ }
+
+ public CommandBindingConfiguration(Commands command, Key key)
+ {
+ Keys = new List() { new KeyConfig(key) };
+ Command = command;
+ }
+
+ public string GetKeysDisplayText()
+ {
+ var s = "";
+
+ foreach (var k in Keys)
+ {
+ var keyString = k.Key.ToString();
+
+ if (keyString.Length == 1)
+ {
+ s += AddKeyWithCtrlOrAlt(k, s, (_, _, _) => k.Shift ? keyString.ToUpper() : keyString.ToLower());
+ }
+ else
+ {
+ s += AddKeyWithCtrlOrAlt(k, s, AddSpecialKey);
+ }
+ }
+
+ return s;
+ }
+
+ private static string AddKeyWithCtrlOrAlt(KeyConfig key, string currentText, Func keyProcessor)
+ {
+ var s = "";
+
+ bool ctrlOrAlt = key.Ctrl || key.Alt;
+
+ if (ctrlOrAlt && currentText.Length > 0 && currentText.Last() != ' ') s += " ";
+
+ if (key.Ctrl) s += "CTRL+";
+ if (key.Alt) s += "ALT+";
+ s += keyProcessor(key, currentText, ctrlOrAlt);
+
+ if (ctrlOrAlt) s += " ";
+
+ return s;
+ }
+
+ private static string AddSpecialKey(KeyConfig key, string currentText, bool wasCtrlOrAlt)
+ {
+ var s = "";
+
+ if (currentText.Length > 0 && currentText.Last() != ' ' && !wasCtrlOrAlt) s += " ";
+ s += key.Key.ToString();
+ if (!wasCtrlOrAlt) s += " ";
+
+ return s;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/KeyBindingConfiguration.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/KeyBindingConfiguration.cs
new file mode 100644
index 0000000..4b2eed6
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/KeyBindingConfiguration.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace FileTime.GuiApp.Configuration
+{
+ public class KeyBindingConfiguration
+ {
+ public bool UseDefaultBindings { get; set; } = true;
+ public List DefaultKeyBindings { get; set; } = new();
+ public List KeyBindings { get; set; } = new();
+ }
+}
\ No newline at end of file
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/KeyConfig.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/KeyConfig.cs
new file mode 100644
index 0000000..8701239
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/KeyConfig.cs
@@ -0,0 +1,22 @@
+using Avalonia.Input;
+
+namespace FileTime.GuiApp.Configuration
+{
+ public class KeyConfig
+ {
+ public Key Key { get; set; }
+ public bool Shift { get; set; }
+ public bool Alt { get; set; }
+ public bool Ctrl { get; set; }
+
+ public KeyConfig() { }
+
+ public KeyConfig(Key key, bool shift = false, bool alt = false, bool ctrl = false)
+ {
+ Key = key;
+ Shift = shift;
+ Alt = alt;
+ Ctrl = ctrl;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/MainConfiguration.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/MainConfiguration.cs
new file mode 100644
index 0000000..64a2e51
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/MainConfiguration.cs
@@ -0,0 +1,128 @@
+using Avalonia.Input;
+using FileTime.App.Core.Command;
+using System;
+using System.Collections.Generic;
+
+namespace FileTime.GuiApp.Configuration
+{
+ public static class MainConfiguration
+ {
+ private static readonly Lazy> _defaultKeybindings = new(InitDefaultKeyBindings);
+
+ public static Dictionary Configuration { get; }
+
+ static MainConfiguration()
+ {
+ Configuration = new();
+ PopulateDefaultEditorPrograms(Configuration);
+ PopulateDefaultKeyBindings(Configuration, _defaultKeybindings.Value, SectionNames.KeybindingSectionName + ":" + nameof(KeyBindingConfiguration.DefaultKeyBindings));
+ }
+
+ private static void PopulateDefaultKeyBindings(Dictionary configuration, List commandBindingConfigs, string basePath)
+ {
+ for (var i = 0; i < commandBindingConfigs.Count; i++)
+ {
+ var baseKey = basePath + $":[{i}]:";
+ var commandBindingConfig = commandBindingConfigs[i];
+ configuration.Add(baseKey + nameof(CommandBindingConfiguration.Command), commandBindingConfig.Command.ToString());
+
+ for (var j = 0; j < commandBindingConfig.Keys.Count; j++)
+ {
+ var key = commandBindingConfig.Keys[j];
+ var keyBaseKey = baseKey + $"keys:[{j}]:";
+ configuration.Add(keyBaseKey + nameof(KeyConfig.Key), key.Key.ToString());
+ configuration.Add(keyBaseKey + nameof(KeyConfig.Shift), key.Shift.ToString());
+ configuration.Add(keyBaseKey + nameof(KeyConfig.Alt), key.Alt.ToString());
+ configuration.Add(keyBaseKey + nameof(KeyConfig.Ctrl), key.Ctrl.ToString());
+ }
+ }
+ }
+
+ private static List InitDefaultKeyBindings()
+ {
+ return new List()
+ {
+ new CommandBindingConfiguration(Commands.AutoRefresh, new KeyConfig(Key.R, shift: true)),
+ new CommandBindingConfiguration(Commands.ChangeTimelineMode, new[] { Key.T, Key.M }),
+ new CommandBindingConfiguration(Commands.CloseTab, Key.Q),
+ new CommandBindingConfiguration(Commands.Compress, new[] { Key.Y, Key.C }),
+ new CommandBindingConfiguration(Commands.Copy, new[] { Key.Y, Key.Y }),
+ new CommandBindingConfiguration(Commands.CopyHash, new[] { Key.C, Key.H }),
+ new CommandBindingConfiguration(Commands.CopyPath, new[] { Key.C, Key.P }),
+ new CommandBindingConfiguration(Commands.CreateContainer, Key.F7),
+ new CommandBindingConfiguration(Commands.CreateContainer, new[] { Key.C, Key.C }),
+ new CommandBindingConfiguration(Commands.CreateElement, new[] { Key.C, Key.E }),
+ new CommandBindingConfiguration(Commands.Cut, new[] { Key.D, Key.D }),
+ new CommandBindingConfiguration(Commands.Edit, new KeyConfig(Key.F4)),
+ new CommandBindingConfiguration(Commands.EnterRapidTravel, new KeyConfig(Key.OemComma, shift: true)),
+ new CommandBindingConfiguration(Commands.FindByName, new[] { Key.F, Key.N }),
+ new CommandBindingConfiguration(Commands.FindByNameRegex, new[] { Key.F, Key.R }),
+ new CommandBindingConfiguration(Commands.GoToHome, new[] { Key.G, Key.H }),
+ new CommandBindingConfiguration(Commands.GoToPath, new KeyConfig(Key.L, ctrl: true)),
+ new CommandBindingConfiguration(Commands.GoToPath, new[] { Key.G, Key.P }),
+ new CommandBindingConfiguration(Commands.GoToProvider, new[] { Key.G, Key.T }),
+ new CommandBindingConfiguration(Commands.GoToRoot, new[] { Key.G, Key.R }),
+ new CommandBindingConfiguration(Commands.HardDelete, new[] { new KeyConfig(Key.D,shift: true), new KeyConfig(Key.D, shift: true) }),
+ new CommandBindingConfiguration(Commands.Mark, Key.Space),
+ new CommandBindingConfiguration(Commands.MoveToLast, new KeyConfig(Key.G, shift: true)),
+ new CommandBindingConfiguration(Commands.MoveToFirst, new[] { Key.G, Key.G }),
+ new CommandBindingConfiguration(Commands.NextTimelineBlock, Key.L ),
+ new CommandBindingConfiguration(Commands.NextTimelineCommand, Key.J ),
+ new CommandBindingConfiguration(Commands.OpenInFileBrowser, new[] { Key.O, Key.E }),
+ new CommandBindingConfiguration(Commands.PasteMerge, new[] { Key.P, Key.P }),
+ new CommandBindingConfiguration(Commands.PasteOverwrite, new[] { Key.P, Key.O }),
+ new CommandBindingConfiguration(Commands.PasteSkip, new[] { Key.P, Key.S }),
+ new CommandBindingConfiguration(Commands.PinFavorite, new[] { Key.F, Key.P }),
+ new CommandBindingConfiguration(Commands.PreviousTimelineBlock, Key.H ),
+ new CommandBindingConfiguration(Commands.PreviousTimelineCommand, Key.K ),
+ new CommandBindingConfiguration(Commands.Refresh, Key.R),
+ new CommandBindingConfiguration(Commands.Rename, Key.F2),
+ new CommandBindingConfiguration(Commands.Rename, new[] { Key.C, Key.W }),
+ new CommandBindingConfiguration(Commands.RunCommand, new KeyConfig(Key.D4, shift: true)),
+ new CommandBindingConfiguration(Commands.ScanContainerSize, new[] { Key.C, Key.S }),
+ new CommandBindingConfiguration(Commands.ShowAllShotcut, Key.F1),
+ new CommandBindingConfiguration(Commands.SoftDelete, new[] { new KeyConfig(Key.D), new KeyConfig(Key.D, shift: true) }),
+ new CommandBindingConfiguration(Commands.SwitchToLastTab, Key.D9),
+ new CommandBindingConfiguration(Commands.SwitchToTab1, Key.D1),
+ new CommandBindingConfiguration(Commands.SwitchToTab2, Key.D2),
+ new CommandBindingConfiguration(Commands.SwitchToTab3, Key.D3),
+ new CommandBindingConfiguration(Commands.SwitchToTab4, Key.D4),
+ new CommandBindingConfiguration(Commands.SwitchToTab5, Key.D5),
+ new CommandBindingConfiguration(Commands.SwitchToTab6, Key.D6),
+ new CommandBindingConfiguration(Commands.SwitchToTab7, Key.D7),
+ new CommandBindingConfiguration(Commands.SwitchToTab8, Key.D8),
+ new CommandBindingConfiguration(Commands.TimelinePause, new[] { Key.T, Key.P }),
+ new CommandBindingConfiguration(Commands.TimelineRefresh, new[] { Key.T, Key.R }),
+ new CommandBindingConfiguration(Commands.TimelineStart, new[] { Key.T, Key.S }),
+ new CommandBindingConfiguration(Commands.ToggleAdvancedIcons, new[] { Key.Z, Key.I }),
+ new CommandBindingConfiguration(Commands.GoUp, Key.Left),
+ new CommandBindingConfiguration(Commands.Open, Key.Right),
+ new CommandBindingConfiguration(Commands.OpenOrRun, Key.Enter),
+ new CommandBindingConfiguration(Commands.MoveCursorUp, Key.Up),
+ new CommandBindingConfiguration(Commands.MoveCursorDown, Key.Down),
+ new CommandBindingConfiguration(Commands.MoveCursorUpPage, Key.PageUp),
+ new CommandBindingConfiguration(Commands.MoveCursorDownPage, Key.PageDown),
+ };
+ }
+
+ private static void PopulateDefaultEditorPrograms(Dictionary configuration)
+ {
+ var editorPrograms = new List()
+ {
+ new ProgramConfiguration(@"c:\Program Files\Notepad++\notepad++.exe"),
+ new ProgramConfiguration("notepad.exe"),
+ };
+
+ for (var i = 0; i < editorPrograms.Count; i++)
+ {
+ if (editorPrograms[i].Path is not string path) continue;
+ configuration.Add($"{SectionNames.ProgramsSectionName}:{nameof(ProgramsConfiguration.DefaultEditorPrograms)}:[{i}]:{nameof(ProgramConfiguration.Path)}", path);
+
+ if (editorPrograms[i].Arguments is string arguments)
+ {
+ configuration.Add($"{SectionNames.ProgramsSectionName}:{nameof(ProgramsConfiguration.DefaultEditorPrograms)}:[{i}]:{nameof(ProgramConfiguration.Arguments)}", arguments);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/ProgramConfiguration.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/ProgramConfiguration.cs
new file mode 100644
index 0000000..83df6da
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/ProgramConfiguration.cs
@@ -0,0 +1,16 @@
+namespace FileTime.GuiApp.Configuration
+{
+ public class ProgramConfiguration
+ {
+ public string? Path { get; set; }
+ public string? Arguments { get; set; }
+
+ public ProgramConfiguration() { }
+
+ public ProgramConfiguration(string? path, string? arguments = null)
+ {
+ Path = path;
+ Arguments = arguments;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/ProgramsConfiguration.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/ProgramsConfiguration.cs
new file mode 100644
index 0000000..446abdf
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/ProgramsConfiguration.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace FileTime.GuiApp.Configuration
+{
+ public class ProgramsConfiguration
+ {
+ public List DefaultEditorPrograms { get; set; } = new();
+ public List EditorPrograms { get; set; } = new();
+ }
+}
\ No newline at end of file
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/SectionNames.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/SectionNames.cs
new file mode 100644
index 0000000..be44f8f
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Configuration/SectionNames.cs
@@ -0,0 +1,8 @@
+namespace FileTime.GuiApp.Configuration
+{
+ public static class SectionNames
+ {
+ public const string KeybindingSectionName = "KeyBindings";
+ public const string ProgramsSectionName = "Programs";
+ }
+}
\ No newline at end of file
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/DateTimeConverter.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/DateTimeConverter.cs
new file mode 100644
index 0000000..3d8b00f
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/DateTimeConverter.cs
@@ -0,0 +1,18 @@
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace FileTime.GuiApp.Converters
+{
+ public class DateTimeConverter : IValueConverter
+ {
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
+ value is DateTime dateTime && parameter is string parameterS
+ ? dateTime.ToString(parameterS)
+ : 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/Avalonia/FileTime.GuiApp/Converters/FormatSizeConverter.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/FormatSizeConverter.cs
new file mode 100644
index 0000000..30d57bc
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/FormatSizeConverter.cs
@@ -0,0 +1,47 @@
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace FileTime.GuiApp.Converters
+{
+ public class FormatSizeConverter : IValueConverter
+ {
+ private const long OneKiloByte = 1024;
+ private const long OneMegaByte = OneKiloByte * 1024;
+ private const long OneGigaByte = OneMegaByte * 1024;
+ private const long OneTerraByte = OneGigaByte * 1024;
+
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return (value, int.TryParse(parameter?.ToString(), out var prec)) switch
+ {
+ (long size, true) => ToSizeString(size, prec),
+ (long size, false) => ToSizeString(size),
+ (null, _) => "...",
+ _ => value
+ };
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static string ToSizeString(long fileSize, int precision = 1)
+ {
+ var fileSizeD = (decimal)fileSize;
+ var (size, suffix) = fileSize switch
+ {
+ > OneTerraByte => (fileSizeD / OneTerraByte, "T"),
+ > OneGigaByte => (fileSizeD / OneGigaByte, "G"),
+ > OneMegaByte => (fileSizeD / OneMegaByte, "M"),
+ > OneKiloByte => (fileSizeD / OneKiloByte, "K"),
+ _ => (fileSizeD, "B")
+ };
+
+ var result = string.Format("{0:N" + precision + "}", size).Replace(',', '.');
+
+ if (result.Contains('.')) result = result.TrimEnd('0').TrimEnd('.');
+ return result + " " + suffix;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/GetFileExtensionConverter.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/GetFileExtensionConverter.cs
new file mode 100644
index 0000000..6d55585
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/GetFileExtensionConverter.cs
@@ -0,0 +1,24 @@
+using System.Globalization;
+using Avalonia.Data.Converters;
+using FileTime.App.Core.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace FileTime.GuiApp.Converters
+{
+ public class GetFileExtensionConverter : IValueConverter
+ {
+ private IItemNameConverterService? _itemNameConverterService;
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is not string fullName) return value;
+ _itemNameConverterService ??= DI.ServiceProvider.GetRequiredService();
+
+ return _itemNameConverterService.GetFileExtension(fullName);
+ }
+
+ 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/Avalonia/FileTime.GuiApp/Converters/ItemViewModeToBrushConverter.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/ItemViewModeToBrushConverter.cs
new file mode 100644
index 0000000..29a8800
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/ItemViewModeToBrushConverter.cs
@@ -0,0 +1,41 @@
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using FileTime.App.Core.Models.Enums;
+
+namespace FileTime.GuiApp.Converters
+{
+ public class ItemViewModeToBrushConverter : IValueConverter
+ {
+ public Brush? DefaultBrush { get; set; }
+ public Brush? AlternativeBrush { get; set; }
+ public Brush? SelectedBrush { get; set; }
+ public Brush? MarkedBrush { get; set; }
+ public Brush? MarkedSelectedBrush { get; set; }
+ public Brush? MarkedAlternativeBrush { 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,
+ ItemViewMode.Marked => MarkedBrush,
+ ItemViewMode.MarkedSelected => MarkedSelectedBrush,
+ ItemViewMode.MarkedAlternative => MarkedAlternativeBrush,
+ _ => 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/Avalonia/FileTime.GuiApp/Converters/ItemViewModelIsAttributeTypeConverter.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/ItemViewModelIsAttributeTypeConverter.cs
new file mode 100644
index 0000000..edadd74
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/ItemViewModelIsAttributeTypeConverter.cs
@@ -0,0 +1,37 @@
+using System.Globalization;
+using Avalonia.Data.Converters;
+using FileTime.App.Core.Models.Enums;
+using FileTime.App.Core.ViewModels;
+
+namespace FileTime.GuiApp.Converters
+{
+ public class ItemViewModelIsAttributeTypeConverter : IValueConverter
+ {
+ public bool Invert { get; set; }
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ var attributeType = GetAttributeType(value);
+ if (parameter == null) return attributeType;
+ var result = parameter is ItemAttributeType targetAttribute && attributeType == targetAttribute;
+ if (Invert && parameter is ItemAttributeType) result = !result;
+ return result;
+ }
+
+ private static ItemAttributeType? GetAttributeType(object? value)
+ {
+ return value switch
+ {
+ IFileViewModel => ItemAttributeType.File,
+ IContainerSizeContainerViewModel => ItemAttributeType.SizeContainer,
+ IElementViewModel => ItemAttributeType.Element,
+ IContainerViewModel => ItemAttributeType.Container,
+ _ => null
+ };
+ }
+
+ 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/Avalonia/FileTime.GuiApp/Converters/NamePartShrinkerConverter.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/NamePartShrinkerConverter.cs
new file mode 100644
index 0000000..fb8d62c
--- /dev/null
+++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Converters/NamePartShrinkerConverter.cs
@@ -0,0 +1,115 @@
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using FileTime.App.Core.Models;
+using FileTime.GuiApp.ViewModels;
+
+namespace FileTime.GuiApp.Converters
+{
+ public class NamePartShrinkerConverter : IMultiValueConverter
+ {
+ private const int PixelPerChar = 8;
+ public object? Convert(IList