Icon, Tab state persistence

This commit is contained in:
2022-02-03 13:08:35 +01:00
parent 1ea1012703
commit 3da246b3ca
28 changed files with 283 additions and 31 deletions

View File

@@ -10,7 +10,7 @@ namespace FileTime.Core.Models
Task<IReadOnlyList<IElement>?> GetElements(CancellationToken token = default);
Task Refresh();
Task<IItem?> GetByPath(string path);
Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false);
Task<IContainer> CreateContainer(string name);
Task<IElement> CreateElement(string name);

View File

@@ -76,7 +76,7 @@ namespace FileTime.Core.Models
?.ToList().AsReadOnly();
}
public async Task<IItem?> GetByPath(string path) => await BaseContainer.GetByPath(path);
public async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false) => await BaseContainer.GetByPath(path, acceptDeepestMatch);
public IContainer? GetParent() => BaseContainer.GetParent();

View File

@@ -49,7 +49,7 @@ namespace FileTime.Core.Providers
public Task Delete() => throw new NotImplementedException();
public Task<IItem?> GetByPath(string path) => throw new NotImplementedException();
public Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false) => throw new NotImplementedException();
public IContainer? GetParent() => null;

View File

@@ -46,7 +46,7 @@ namespace FileTime.Core.Timeline
public Task Delete() => Task.CompletedTask;
public async Task<IItem?> GetByPath(string path)
public async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
{
var paths = path.Split(Constants.SeparatorChar);
@@ -59,7 +59,7 @@ namespace FileTime.Core.Timeline
if (item is IContainer container)
{
return await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)));
return await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)), acceptDeepestMatch);
}
return null;

View File

@@ -50,7 +50,7 @@ namespace FileTime.Core.Timeline
public Task Delete() => throw new NotSupportedException();
public Task<IItem?> GetByPath(string path)
public Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
{
throw new NotImplementedException();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,12 @@
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" xml:space="preserve"
xmlns="http://www.w3.org/2000/svg">
<path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#FFD767" fill-rule="nonzero"/>
<line x1="4" y1="9" x2="7" y2="9" stroke="#444444" stroke-width="2"/>
<line x1="8" y1="9" x2="14" y2="9" stroke="#444444" stroke-width="2"/>
<line x1="15" y1="9" x2="20" y2="9" stroke="#444444" stroke-width="2"/>
<line x1="8" y1="13" x2="14" y2="13" stroke="#444444" stroke-width="2"/>
<line x1="15" y1="13" x2="20" y2="13" stroke="#444444" stroke-width="2"/>
<line x1="8" y1="17" x2="14" y2="17" stroke="#444444" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

Binary file not shown.

View File

@@ -16,6 +16,7 @@ namespace FileTime.Avalonia.Application
private ObservableCollection<TabContainer> _tabs = new();
[Property]
[PropertyCallMethod(nameof(SelectedTabChanged))]
private TabContainer _selectedTab;
[Property]
@@ -62,6 +63,14 @@ namespace FileTime.Avalonia.Application
}
}
private void SelectedTabChanged()
{
foreach(var tab in Tabs)
{
tab.IsSelected = tab == SelectedTab;
}
}
private async Task TabItemMarked(TabState tabState, AbsolutePath item)
{
var tabContainer = Tabs.FirstOrDefault(t => t.TabState == tabState);

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -3,12 +3,16 @@
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationIcon>Assets\filetime.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
<None Remove=".gitignore" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\filetime.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.12" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.12" />

View File

@@ -0,0 +1,7 @@
namespace FileTime.Avalonia.Models.Persistence
{
public class PersistenceRoot
{
public TabStates? TabStates { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using FileTime.Avalonia.Application;
namespace FileTime.Avalonia.Models.Persistence
{
public class TabState
{
public string? Path { get; set; }
public int Number { get; set; }
public TabState() { }
public TabState(TabContainer tab)
{
Path = tab.CurrentLocation.Item.FullName;
Number = tab.TabNumber;
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace FileTime.Avalonia.Models.Persistence
{
public class TabStates
{
public List<TabState>? Tabs { get; set; }
public int? ActiveTabNumber { get; set; }
}
}

View File

@@ -0,0 +1,131 @@
using System.Linq;
using System.Net;
using System.Text;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using FileTime.Avalonia.Application;
using FileTime.Avalonia.Models.Persistence;
using System;
using FileTime.Core.Components;
using FileTime.Core.Providers;
using FileTime.Providers.Local;
using FileTime.Core.Models;
namespace FileTime.Avalonia.Services
{
public class StatePersistenceService
{
private readonly AppState _appState;
private readonly ItemNameConverterService _itemNameConverterService;
private readonly JsonSerializerOptions _jsonOptions;
private readonly string _settingsPath;
private readonly IEnumerable<IContentProvider> _contentProviders;
private readonly LocalContentProvider _localContentProvider;
public StatePersistenceService(
AppState appState,
ItemNameConverterService itemNameConverterService,
IEnumerable<IContentProvider> contentProviders,
LocalContentProvider localContentProvider)
{
_appState = appState;
_itemNameConverterService = itemNameConverterService;
_contentProviders = contentProviders;
_localContentProvider = localContentProvider;
_settingsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FileTime", "savedState.json");
_jsonOptions = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
};
}
public async Task LoadStatesAsync()
{
if (!File.Exists(_settingsPath)) return;
using var stateReader = File.OpenRead(_settingsPath);
var state = await JsonSerializer.DeserializeAsync<PersistenceRoot>(stateReader);
if (state != null)
{
await RestoreTabs(state.TabStates);
}
}
public async Task SaveStatesAsync()
{
var state = new PersistenceRoot
{
TabStates = SerializeTabStates()
};
var settingsDirectory = new DirectoryInfo(string.Join(Path.DirectorySeparatorChar, _settingsPath.Split(Path.DirectorySeparatorChar)[0..^1]));
if (!settingsDirectory.Exists) settingsDirectory.Create();
using var stateWriter = File.OpenWrite(_settingsPath);
await JsonSerializer.SerializeAsync(stateWriter, state, _jsonOptions);
}
private TabStates SerializeTabStates()
{
var tabStates = new List<TabState>();
foreach (var tab in _appState.Tabs)
{
tabStates.Add(new TabState(tab));
}
return new TabStates()
{
Tabs = tabStates,
ActiveTabNumber = _appState.SelectedTab.TabNumber
};
}
private async Task<bool> RestoreTabs(TabStates? tabStates)
{
if (tabStates == null
|| tabStates.Tabs == null)
{
return false;
}
foreach (var tab in tabStates.Tabs)
{
if (tab.Path == null) continue;
IItem? pathItem = null;
foreach (var contentProvider in _contentProviders)
{
if (contentProvider.CanHandlePath(tab.Path))
{
pathItem = await contentProvider.GetByPath(tab.Path, true);
if (pathItem != null) break;
}
}
var container = pathItem switch
{
IContainer c => c,
IElement e => e.GetParent(),
_ => null
};
if (container == null) continue;
var newTab = new Tab();
await newTab.Init(container);
var newTabContainer = new TabContainer(newTab, _localContentProvider, _itemNameConverterService);
await newTabContainer.Init(tab.Number);
_appState.Tabs.Add(newTabContainer);
}
if (_appState.Tabs.FirstOrDefault(t => t.TabNumber == tabStates.ActiveTabNumber) is TabContainer tabContainer)
{
_appState.SelectedTab = tabContainer;
}
return true;
}
}
}

View File

@@ -23,6 +23,7 @@ namespace FileTime.Avalonia
serviceCollection = serviceCollection
.AddLogging()
.AddSingleton<ItemNameConverterService>()
.AddSingleton<StatePersistenceService>()
.AddSingleton<IIconProvider, MaterialIconProvider>();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))

View File

@@ -32,6 +32,7 @@ namespace FileTime.Avalonia.ViewModels
[ViewModel]
[Inject(typeof(LocalContentProvider))]
[Inject(typeof(AppState), PropertyAccessModifier = AccessModifier.Public)]
[Inject(typeof(StatePersistenceService), PropertyName = "StatePersistence", PropertyAccessModifier = AccessModifier.Public)]
[Inject(typeof(ItemNameConverterService))]
public partial class MainPageViewModel
{
@@ -90,6 +91,7 @@ namespace FileTime.Avalonia.ViewModels
var inputInterface = (BasicInputHandler)App.ServiceProvider.GetService<IInputInterface>()!;
inputInterface.InputHandler = ReadInputs;
App.ServiceProvider.GetService<TopContainer>();
await StatePersistence.LoadStatesAsync();
_timeRunner.CommandsChanged += UpdateParalellCommands;
InitCommandBindings();
@@ -100,9 +102,13 @@ namespace FileTime.Avalonia.ViewModels
_keysToSkip.Add(new KeyWithModifiers[] { new KeyWithModifiers(Key.PageDown) });
_keysToSkip.Add(new KeyWithModifiers[] { new KeyWithModifiers(Key.PageUp) });
_keysToSkip.Add(new KeyWithModifiers[] { new KeyWithModifiers(Key.F4, alt: true) });
_keysToSkip.Add(new KeyWithModifiers[] { new KeyWithModifiers(Key.LWin) });
_keysToSkip.Add(new KeyWithModifiers[] { new KeyWithModifiers(Key.RWin) });
AllShortcut = _commandBindings.Concat(_universalCommandBindings).ToList();
if (AppState.Tabs.Count == 0)
{
var tab = new Tab();
await tab.Init(LocalContentProvider);
@@ -110,6 +116,7 @@ namespace FileTime.Avalonia.ViewModels
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))
@@ -355,11 +362,6 @@ namespace FileTime.Avalonia.ViewModels
}
AppState.SelectedTab = tabContainer;
foreach (var tab2 in AppState.Tabs)
{
tab2.IsSelected = tab2.TabNumber == tabContainer!.TabNumber;
}
}
public async Task CloseTab()
@@ -656,6 +658,42 @@ namespace FileTime.Avalonia.ViewModels
return Task.CompletedTask;
}
private Task OpenInDefaultFileExplorer()
{
if (AppState.SelectedTab.CurrentLocation.Container is LocalFolder localFolder)
{
var path = localFolder.Directory.FullName;
if (path != null)
{
Process.Start("explorer.exe", "\"" + path + "\"");
}
}
return Task.CompletedTask;
}
private async Task CopyPath()
{
string? textToCopy = null;
if (AppState.SelectedTab.CurrentLocation.Container is LocalFolder localFolder)
{
textToCopy = localFolder.Directory.FullName;
}
if (AppState.SelectedTab.CurrentLocation.Container is LocalFile localFile)
{
textToCopy = localFile.File.FullName;
}
else if (AppState.SelectedTab.CurrentLocation.Container.FullName is string fullName)
{
textToCopy = fullName;
}
if(textToCopy != null && global::Avalonia.Application.Current?.Clipboard is not null)
{
await global::Avalonia.Application.Current.Clipboard.SetTextAsync(textToCopy);
}
}
private Task ShowAllShortcut2()
{
ShowAllShortcut = true;
@@ -1081,6 +1119,18 @@ namespace FileTime.Avalonia.ViewModels
FileTime.App.Core.Command.Commands.Dummy,
new KeyWithModifiers[] { new KeyWithModifiers(Key.F1) },
ShowAllShortcut2),
//TODO REMOVE
new CommandBinding(
"open in default file browser",
FileTime.App.Core.Command.Commands.Dummy,
new KeyWithModifiers[] { new KeyWithModifiers(Key.O), new KeyWithModifiers(Key.E) },
OpenInDefaultFileExplorer),
//TODO REMOVE
new CommandBinding(
"copy path",
FileTime.App.Core.Command.Commands.Dummy,
new KeyWithModifiers[] { new KeyWithModifiers(Key.C), new KeyWithModifiers(Key.P) },
CopyPath),
};
var universalCommandBindings = new List<CommandBinding>()
{

View File

@@ -6,12 +6,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:FileTime.Avalonia.ViewModels"
xmlns:local="using:FileTime.Avalonia.Views"
Title="FileTime.Avalonia"
Title="FileTime"
d:DesignHeight="450"
d:DesignWidth="800"
Icon="/Assets/avalonia-logo.ico"
Icon="/Assets/filetime.ico"
InputElement.KeyDown="OnKeyDown"
InputElement.KeyUp="OnKeyUp"
Closed="OnWindowClosed"
mc:Ignorable="d">
<Grid
@@ -188,7 +189,10 @@
<TextBlock
VerticalAlignment="Center" Text="{Binding TabNumber,StringFormat=({0})}" />
<local:PathPresenter Margin="5,0,0,0" DataContext="{Binding CurrentLocation.Container.FullName}"/>
<TextBlock
VerticalAlignment="Center" Margin="5,0,0,0" Text="{Binding CurrentLocation.Container.Name}" />
<!--local:PathPresenter Margin="5,0,0,0" DataContext="{Binding CurrentLocation.Container.FullName}"/-->
</StackPanel>
<Rectangle Fill="{DynamicResource ForegroundBrush}" Grid.Row="1" IsVisible="{Binding IsSelected}"/>

View File

@@ -6,6 +6,7 @@ using Avalonia.Markup.Xaml;
using FileTime.Avalonia.Misc;
using FileTime.Avalonia.Models;
using FileTime.Avalonia.ViewModels;
using System;
using System.Linq;
namespace FileTime.Avalonia.Views
@@ -107,5 +108,10 @@ namespace FileTime.Avalonia.Views
e.Handled = true;
}
}
private void OnWindowClosed(object sender, EventArgs e)
{
ViewModel?.StatePersistence.SaveStatesAsync().Wait();
}
}
}

View File

@@ -47,7 +47,7 @@ namespace FileTime.Providers.Local
_items = _rootContainers.Cast<IItem>().ToList().AsReadOnly();
}
public async Task<IItem?> GetByPath(string path)
public async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
{
path = path.Replace(Path.DirectorySeparatorChar, Constants.SeparatorChar).TrimEnd(Constants.SeparatorChar);
var pathParts = (IsCaseInsensitive ? path.ToLower() : path).TrimStart(Constants.SeparatorChar).Split(Constants.SeparatorChar);
@@ -64,7 +64,7 @@ namespace FileTime.Providers.Local
}
var remainingPath = string.Join(Constants.SeparatorChar, pathParts.Skip(1));
return remainingPath.Length == 0 ? rootContainer : await rootContainer.GetByPath(remainingPath);
return remainingPath.Length == 0 ? rootContainer : await rootContainer.GetByPath(remainingPath, acceptDeepestMatch);
}
public async Task Refresh() => await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty);

View File

@@ -88,7 +88,7 @@ namespace FileTime.Providers.Local
return _elements;
}
public async Task<IItem?> GetByPath(string path)
public async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
{
var paths = path.Split(Constants.SeparatorChar);
@@ -101,7 +101,7 @@ namespace FileTime.Providers.Local
if (item is IContainer container)
{
return await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)));
return await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)), acceptDeepestMatch);
}
return null;

View File

@@ -64,7 +64,7 @@ namespace FileTime.Providers.Smb
throw new NotSupportedException();
}
public async Task<IItem?> GetByPath(string path)
public async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
{
if (path == null) return this;
@@ -78,7 +78,7 @@ namespace FileTime.Providers.Smb
}
var remainingPath = string.Join(Constants.SeparatorChar, pathParts.Skip(1));
return remainingPath.Length == 0 ? rootContainer : await rootContainer.GetByPath(remainingPath);
return remainingPath.Length == 0 ? rootContainer : await rootContainer.GetByPath(remainingPath, acceptDeepestMatch);
}
public IContainer? GetParent() => _parent;

View File

@@ -51,7 +51,7 @@ namespace FileTime.Providers.Smb
public Task<IContainer> Clone() => Task.FromResult((IContainer)this);
public async Task<IItem?> GetByPath(string path)
public async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
{
var paths = path.Split(Constants.SeparatorChar);
@@ -64,7 +64,7 @@ namespace FileTime.Providers.Smb
if (item is IContainer container)
{
return await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)));
return await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)), acceptDeepestMatch);
}
return null;

View File

@@ -77,7 +77,7 @@ namespace FileTime.Providers.Smb
return Task.CompletedTask;
}
public Task<IItem?> GetByPath(string path)
public Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
{
throw new NotImplementedException();
}

View File

@@ -70,7 +70,7 @@ namespace FileTime.Providers.Smb
throw new NotImplementedException();
}
public async Task<IItem?> GetByPath(string path)
public async Task<IItem?> GetByPath(string path, bool acceptDeepestMatch = false)
{
var paths = path.Split(Constants.SeparatorChar);
@@ -83,7 +83,7 @@ namespace FileTime.Providers.Smb
if (item is IContainer container)
{
return await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)));
return await container.GetByPath(string.Join(Constants.SeparatorChar, paths.Skip(1)), acceptDeepestMatch);
}
return null;