Tab restore

This commit is contained in:
2022-05-24 17:02:36 +02:00
parent dcff003c28
commit 8167909781
16 changed files with 339 additions and 11 deletions

View File

@@ -0,0 +1,7 @@
namespace FileTime.App.Core.Models;
public interface IApplicationSettings
{
string AppDataRoot { get; }
string EnvironmentName { get; }
}

View File

@@ -2,4 +2,5 @@ namespace FileTime.App.Core.Services;
public interface IStartupHandler public interface IStartupHandler
{ {
Task InitAsync();
} }

View File

@@ -0,0 +1,6 @@
namespace FileTime.App.Core.Services.Persistence;
public interface ITabPersistenceService : IStartupHandler, IExitHandler
{
void SaveStates();
}

View File

@@ -24,6 +24,8 @@ public interface ITabViewModel : IInitable<ITab, int>, IDisposable
IObservable<IReadOnlyCollection<IItemViewModel>?> CurrentItemsCollectionObservable { get; } IObservable<IReadOnlyCollection<IItemViewModel>?> CurrentItemsCollectionObservable { get; }
IObservable<IReadOnlyCollection<IItemViewModel>?> ParentsChildrenCollectionObservable { get; } IObservable<IReadOnlyCollection<IItemViewModel>?> ParentsChildrenCollectionObservable { get; }
IObservable<IReadOnlyCollection<IItemViewModel>?> SelectedsChildrenCollectionObservable { get; } IObservable<IReadOnlyCollection<IItemViewModel>?> SelectedsChildrenCollectionObservable { get; }
IContainer? CachedCurrentLocation { get; }
void ClearMarkedItems(); void ClearMarkedItems();
void RemoveMarkedItem(FullName fullName); void RemoveMarkedItem(FullName fullName);
void AddMarkedItem(FullName fullName); void AddMarkedItem(FullName fullName);

View File

@@ -0,0 +1,54 @@
using System.Reflection;
namespace FileTime.App.Core.Models;
public class ApplicationSettings : IApplicationSettings
{
public string AppDataRoot { get; private set; } = null!;
public string EnvironmentName { get; private set; } = null!;
public ApplicationSettings()
{
#if DEBUG
InitDebugSettings();
#else
InitReleaseSettings();
#endif
}
private void InitDebugSettings()
{
EnvironmentName = "Development";
AppDataRoot = Path.Combine(Environment.CurrentDirectory, "appdata");
}
private void InitReleaseSettings()
{
EnvironmentName = "Release";
var possibleDataRootsPaths = new List<string>()
{
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();
appDataRoot = possibleAppDataRoot;
break;
}
catch
{
}
}
AppDataRoot = appDataRoot ?? throw new UnauthorizedAccessException();
}
}

View File

@@ -0,0 +1,215 @@
using System.Reactive.Linq;
using System.Text.Json;
using FileTime.App.Core.Models;
using FileTime.App.Core.ViewModels;
using FileTime.Core.Models;
using FileTime.Core.Services;
using FileTime.Core.Timeline;
using FileTime.Providers.Local;
using InitableService;
using Microsoft.Extensions.Logging;
namespace FileTime.App.Core.Services.Persistence;
public class TabPersistenceService : ITabPersistenceService
{
private readonly IAppState _appState;
private readonly ILogger<TabPersistenceService> _logger;
private class PersistenceRoot
{
public TabStates? TabStates { get; set; }
}
private class TabStates
{
public List<TabState>? Tabs { get; set; }
public int? ActiveTabNumber { get; set; }
}
private class TabState
{
public string? Path { get; set; }
public int Number { get; set; }
public TabState()
{
}
public TabState(FullName path, int number)
{
Path = path.Path;
Number = number;
}
}
private readonly string _settingsPath;
private readonly JsonSerializerOptions _jsonOptions;
private readonly ITimelessContentProvider _timelessContentProvider;
private readonly IServiceProvider _serviceProvider;
private readonly ILocalContentProvider _localContentProvider;
public TabPersistenceService(
IApplicationSettings applicationSettings,
IAppState appState,
ITimelessContentProvider timelessContentProvider,
IServiceProvider serviceProvider,
ILocalContentProvider localContentProvider,
ILogger<TabPersistenceService> logger)
{
_appState = appState;
_logger = logger;
_settingsPath = Path.Combine(applicationSettings.AppDataRoot, "savedState.json");
_timelessContentProvider = timelessContentProvider;
_serviceProvider = serviceProvider;
_localContentProvider = localContentProvider;
_jsonOptions = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true,
WriteIndented = true
};
}
public Task ExitAsync()
{
SaveStates();
return Task.CompletedTask;
}
private async Task LoadStatesAsync()
{
if (!File.Exists(_settingsPath)) return;
try
{
await using var stateReader = File.OpenRead(_settingsPath);
var state = await JsonSerializer.DeserializeAsync<PersistenceRoot>(stateReader);
if (state != null)
{
await RestoreTabs(state.TabStates);
}
}
catch (Exception e)
{
_logger.LogError(e, "Unknown exception while restoring app state");
}
}
private async Task RestoreTabs(TabStates? tabStates)
{
if (tabStates == null
|| tabStates.Tabs == null)
{
CreateEmptyTab();
return;
}
try
{
foreach (var tab in tabStates.Tabs)
{
try
{
if (tab.Path == null) continue;
IContainer? container = null;
var path = new FullName(tab.Path);
while (true)
{
try
{
var pathItem = await _timelessContentProvider.GetItemByFullNameAsync(path, PointInTime.Present);
container = pathItem switch
{
IContainer c => c,
IElement e => e.Parent?.ResolveAsync() as IContainer,
_ => null
};
break;
}
catch
{
path = path.GetParent();
if (path == null)
{
throw new Exception($"Could not find an initializable path along {tab.Path}");
}
}
}
if (container == null) continue;
var tabToLoad = _serviceProvider.GetInitableResolver(container)
.GetRequiredService<ITab>();
var tabViewModel = _serviceProvider.GetInitableResolver(tabToLoad, tab.Number).GetRequiredService<ITabViewModel>();
_appState.AddTab(tabViewModel);
}
catch (Exception e)
{
_logger.LogError(e, "Unkown exception while restoring tab. {TabState}", JsonSerializer.Serialize(tab, _jsonOptions));
}
}
}
catch (Exception e)
{
_logger.LogError(e, "Unkown exception while restoring tabs.");
}
if (_appState.Tabs.Count == 0)
{
CreateEmptyTab();
}
else
{
var tabToActivate = _appState.Tabs.FirstOrDefault(t => t.TabNumber == tabStates.ActiveTabNumber);
if (tabToActivate is not null) _appState.SetSelectedTab(tabToActivate);
}
void CreateEmptyTab()
{
var tab = _serviceProvider.GetInitableResolver<IContainer>(_localContentProvider)
.GetRequiredService<ITab>();
var tabViewModel = _serviceProvider.GetInitableResolver(tab, 1).GetRequiredService<ITabViewModel>();
_appState.AddTab(tabViewModel);
}
}
public void SaveStates()
{
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();
var serializedData = JsonSerializer.Serialize(state, _jsonOptions);
File.WriteAllText(_settingsPath, serializedData);
}
private TabStates SerializeTabStates()
{
var tabStates = new List<TabState>();
foreach (var tab in _appState.Tabs)
{
var currentLocation = tab.CachedCurrentLocation;
if (currentLocation is null) continue;
tabStates.Add(new TabState(currentLocation.FullName!, tab.TabNumber));
}
return new TabStates()
{
Tabs = tabStates,
ActiveTabNumber = _appState.CurrentSelectedTab?.TabNumber
};
}
public async Task InitAsync()
{
await LoadStatesAsync();
}
}

View File

@@ -22,9 +22,11 @@ public static class Startup
serviceCollection.TryAddSingleton<IUserCommandHandlerService, UserCommandHandlerService>(); serviceCollection.TryAddSingleton<IUserCommandHandlerService, UserCommandHandlerService>();
serviceCollection.TryAddSingleton<IClipboardService, ClipboardService>(); serviceCollection.TryAddSingleton<IClipboardService, ClipboardService>();
serviceCollection.TryAddSingleton<IIdentifiableUserCommandService, IdentifiableUserCommandService>(); serviceCollection.TryAddSingleton<IIdentifiableUserCommandService, IdentifiableUserCommandService>();
serviceCollection.TryAddSingleton<IStartupHandler, DefaultIdentifiableCommandHandlerRegister>();
serviceCollection.TryAddSingleton<IItemPreviewService, ItemPreviewService>(); serviceCollection.TryAddSingleton<IItemPreviewService, ItemPreviewService>();
return serviceCollection.AddCommandHandlers();
return serviceCollection
.AddCommandHandlers()
.AddSingleton<IStartupHandler, DefaultIdentifiableCommandHandlerRegister>();
} }
private static IServiceCollection AddCommandHandlers(this IServiceCollection serviceCollection) private static IServiceCollection AddCommandHandlers(this IServiceCollection serviceCollection)

View File

@@ -37,6 +37,8 @@ public class DefaultIdentifiableCommandHandlerRegister : IStartupHandler
AddUserCommand(SwitchToTabCommand.SwitchToTab8); AddUserCommand(SwitchToTabCommand.SwitchToTab8);
} }
public Task InitAsync() => Task.CompletedTask;
private void AddUserCommand(IIdentifiableUserCommand command) private void AddUserCommand(IIdentifiableUserCommand command)
=> _service.AddIdentifiableUserCommandFactory(command.UserCommandID, () => command); => _service.AddIdentifiableUserCommandFactory(command.UserCommandID, () => command);
} }

View File

@@ -43,7 +43,8 @@ public partial class TabViewModel : ITabViewModel
null!; null!;
public IObservable<IReadOnlyCollection<IItemViewModel>?> public IObservable<IReadOnlyCollection<IItemViewModel>?>
SelectedsChildrenCollectionObservable { get; private set; } = null!; SelectedsChildrenCollectionObservable
{ get; private set; } = null!;
[Property] private BindedCollection<IItemViewModel>? _currentItemsCollection; [Property] private BindedCollection<IItemViewModel>? _currentItemsCollection;
@@ -51,6 +52,8 @@ public partial class TabViewModel : ITabViewModel
[Property] private BindedCollection<IItemViewModel>? _selectedsChildrenCollection; [Property] private BindedCollection<IItemViewModel>? _selectedsChildrenCollection;
public IContainer? CachedCurrentLocation { get; private set; }
public TabViewModel( public TabViewModel(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
IItemNameConverterService itemNameConverterService, IItemNameConverterService itemNameConverterService,
@@ -74,6 +77,8 @@ public partial class TabViewModel : ITabViewModel
tab.AddToDisposables(_disposables); tab.AddToDisposables(_disposables);
CurrentLocation = tab.CurrentLocation.AsObservable(); CurrentLocation = tab.CurrentLocation.AsObservable();
CurrentLocation.Subscribe(l => CachedCurrentLocation = l).AddToDisposables(_disposables);
CurrentItems = tab.CurrentItems CurrentItems = tab.CurrentItems
.Select(items => items?.Transform(i => MapItemToViewModel(i, ItemViewModelType.Main))) .Select(items => items?.Transform(i => MapItemToViewModel(i, ItemViewModelType.Main)))
/*.ObserveOn(_rxSchedulerService.GetWorkerScheduler()) /*.ObserveOn(_rxSchedulerService.GetWorkerScheduler())

View File

@@ -1,4 +1,7 @@
using FileTime.App.Core; using FileTime.App.Core;
using FileTime.App.Core.Models;
using FileTime.App.Core.Services;
using FileTime.App.Core.Services.Persistence;
using FileTime.Core.Command; using FileTime.Core.Command;
using FileTime.Core.Command.CreateContainer; using FileTime.Core.Command.CreateContainer;
using FileTime.Core.Command.CreateElement; using FileTime.Core.Command.CreateElement;
@@ -21,8 +24,12 @@ public static class DependencyInjection
serviceCollection.TryAddSingleton<ITimelessContentProvider, TimelessContentProvider>(); serviceCollection.TryAddSingleton<ITimelessContentProvider, TimelessContentProvider>();
serviceCollection.TryAddSingleton<ICommandRunner, CommandRunner>(); serviceCollection.TryAddSingleton<ICommandRunner, CommandRunner>();
serviceCollection.TryAddSingleton<IContentAccessorFactory, ContentAccessorFactory>(); serviceCollection.TryAddSingleton<IContentAccessorFactory, ContentAccessorFactory>();
serviceCollection.TryAddSingleton<ITab, Tab>();
serviceCollection.TryAddSingleton<ILocalCommandExecutor, LocalCommandExecutor>(); serviceCollection.TryAddSingleton<ILocalCommandExecutor, LocalCommandExecutor>();
serviceCollection.TryAddSingleton<IApplicationSettings, ApplicationSettings>();
serviceCollection.TryAddSingleton<ITabPersistenceService, TabPersistenceService>();
serviceCollection.TryAddTransient<ITab, Tab>();
serviceCollection.AddSingleton<IExitHandler, ITabPersistenceService>(sp => sp.GetRequiredService<ITabPersistenceService>());
serviceCollection.AddSingleton<IStartupHandler, ITabPersistenceService>(sp => sp.GetRequiredService<ITabPersistenceService>());
return serviceCollection return serviceCollection
.AddCoreAppServices() .AddCoreAppServices()

View File

@@ -35,13 +35,14 @@ public static class Startup
serviceCollection.TryAddSingleton<IDefaultModeKeyInputHandler, DefaultModeKeyInputHandler>(); serviceCollection.TryAddSingleton<IDefaultModeKeyInputHandler, DefaultModeKeyInputHandler>();
serviceCollection.TryAddSingleton<IKeyboardConfigurationService, KeyboardConfigurationService>(); serviceCollection.TryAddSingleton<IKeyboardConfigurationService, KeyboardConfigurationService>();
serviceCollection.TryAddSingleton<IRapidTravelModeKeyInputHandler, RapidTravelModeKeyInputHandler>(); serviceCollection.TryAddSingleton<IRapidTravelModeKeyInputHandler, RapidTravelModeKeyInputHandler>();
serviceCollection.TryAddSingleton<IStartupHandler, RootDriveInfoService>();
serviceCollection.TryAddSingleton<LifecycleService>(); serviceCollection.TryAddSingleton<LifecycleService>();
serviceCollection.TryAddSingleton<IIconProvider, MaterialIconProvider>(); serviceCollection.TryAddSingleton<IIconProvider, MaterialIconProvider>();
serviceCollection.TryAddSingleton<IModalService, ModalService>(); serviceCollection.TryAddSingleton<IModalService, ModalService>();
serviceCollection.TryAddSingleton<IDialogService, DialogService>(); serviceCollection.TryAddSingleton<IDialogService, DialogService>();
serviceCollection.TryAddSingleton<IInputInterface>(s => s.GetRequiredService<IDialogService>()); serviceCollection.TryAddSingleton<IInputInterface>(s => s.GetRequiredService<IDialogService>());
return serviceCollection;
return serviceCollection
.AddSingleton<IStartupHandler, RootDriveInfoService>();
} }
internal static IServiceCollection RegisterLogging(this IServiceCollection serviceCollection) internal static IServiceCollection RegisterLogging(this IServiceCollection serviceCollection)

View File

@@ -5,13 +5,23 @@ namespace FileTime.GuiApp.Services;
public class LifecycleService public class LifecycleService
{ {
private readonly IEnumerable<IExitHandler> _exitHandlers; private readonly IEnumerable<IExitHandler> _exitHandlers;
private readonly IEnumerable<IStartupHandler> _startupHandlers;
public LifecycleService(IEnumerable<IStartupHandler> startupHandlers, IEnumerable<IExitHandler> exitHandlers) public LifecycleService(IEnumerable<IStartupHandler> startupHandlers, IEnumerable<IExitHandler> exitHandlers)
{ {
_exitHandlers = exitHandlers; _exitHandlers = exitHandlers;
_startupHandlers = startupHandlers;
} }
public async Task Exit() public async Task InitStartupHandlersAsync()
{
foreach (var startupHandler in _startupHandlers)
{
await startupHandler.InitAsync();
}
}
public async Task ExitAsync()
{ {
foreach (var exitHandler in _exitHandlers) foreach (var exitHandler in _exitHandlers)
{ {

View File

@@ -38,7 +38,7 @@ public class RootDriveInfoService : IStartupHandler
{ {
var containerPath = localContentProvider.GetNativePath(i.Path).Path; var containerPath = localContentProvider.GetNativePath(i.Path).Path;
var drivePath = d.Name.TrimEnd(Path.DirectorySeparatorChar); var drivePath = d.Name.TrimEnd(Path.DirectorySeparatorChar);
return containerPath == drivePath return containerPath == drivePath
|| (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && containerPath == "/" && d.Name == "/"); || (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && containerPath == "/" && d.Name == "/");
}))) })))
.Filter(t => t.Drive is not null); .Filter(t => t.Drive is not null);
@@ -68,4 +68,6 @@ public class RootDriveInfoService : IStartupHandler
_rootDrives.AddRange(drives); _rootDrives.AddRange(drives);
} }
} }
public Task InitAsync() => Task.CompletedTask;
} }

View File

@@ -50,14 +50,16 @@ public partial class MainWindowViewModel : IMainWindowViewModelBase
Title = "FileTime " + versionString; Title = "FileTime " + versionString;
//TODO: refactor //TODO: refactor
if (AppState.Tabs.Count == 0) /*if (AppState.Tabs.Count == 0)
{ {
var tab = _serviceProvider.GetInitableResolver<IContainer>(_localContentProvider) var tab = _serviceProvider.GetInitableResolver<IContainer>(_localContentProvider)
.GetRequiredService<ITab>(); .GetRequiredService<ITab>();
var tabViewModel = _serviceProvider.GetInitableResolver(tab, 1).GetRequiredService<ITabViewModel>(); var tabViewModel = _serviceProvider.GetInitableResolver(tab, 1).GetRequiredService<ITabViewModel>();
_appState.AddTab(tabViewModel); _appState.AddTab(tabViewModel);
} }*/
_lifecycleService.InitStartupHandlersAsync().Wait();
} }
public void ProcessKeyDown(Key key, KeyModifiers keyModifiers, Action<bool> setHandled) public void ProcessKeyDown(Key key, KeyModifiers keyModifiers, Action<bool> setHandled)
@@ -72,4 +74,9 @@ public partial class MainWindowViewModel : IMainWindowViewModelBase
await UserCommandHandlerService.HandleCommandAsync( await UserCommandHandlerService.HandleCommandAsync(
new OpenContainerCommand(new AbsolutePath(_timelessContentProvider, resolvedContainer))); new OpenContainerCommand(new AbsolutePath(_timelessContentProvider, resolvedContainer)));
} }
public async Task OnExit()
{
await _lifecycleService.ExitAsync();
}
} }

View File

@@ -22,6 +22,7 @@
Icon="/Assets/filetime.ico" Icon="/Assets/filetime.ico"
InputElement.KeyDown="OnKeyDown" InputElement.KeyDown="OnKeyDown"
Opened="OnWindowOpened" Opened="OnWindowOpened"
Closed="OnWindowClosed"
TransparencyLevelHint="Blur" TransparencyLevelHint="Blur"
mc:Ignorable="d"> mc:Ignorable="d">

View File

@@ -82,7 +82,7 @@ public partial class MainWindow : Window
&& sender is StyledElement control) && sender is StyledElement control)
{ {
FullName? path = null; FullName? path = null;
if (control.DataContext is IHaveFullPath { Path: { } } hasFullPath) if (control.DataContext is IHaveFullPath {Path: { }} hasFullPath)
{ {
path = hasFullPath.Path; path = hasFullPath.Path;
} }
@@ -108,4 +108,10 @@ public partial class MainWindow : Window
e.Handled = true; e.Handled = true;
} }
} }
private void OnWindowClosed(object? sender, EventArgs e)
{
var vm = ViewModel;
Task.Run(() => vm?.OnExit()).Wait();
}
} }