Multi instance, tab handling

This commit is contained in:
2023-08-29 18:03:22 +02:00
parent 85488c293d
commit 28640e5dd2
33 changed files with 1430 additions and 436 deletions

View File

@@ -0,0 +1,7 @@
using FileTime.Core.Models;
namespace FileTime.App.Core.Models;
public record TabToOpen(int? TabNumber, NativePath Path);
public record TabsToOpenOnStart(List<TabToOpen> TabsToOpen);

View File

@@ -0,0 +1,27 @@
using FileTime.Core.Models;
namespace FileTime.App.Core.UserCommand;
public class NewTabCommand : IUserCommand
{
public NewTabCommand(FullName? path)
{
Path = path;
}
public FullName? Path { get; }
public bool Open { get; init; } = true;
}
public class IdentifiableNewTabCommand : NewTabCommand, IIdentifiableUserCommand
{
public const string CommandName = "new_tab";
public static IdentifiableNewTabCommand Instance { get; } = new();
private IdentifiableNewTabCommand():base(null)
{
}
public string UserCommandID => CommandName;
public string Title => "New tab";
}

View File

@@ -0,0 +1,15 @@
namespace FileTime.App.Core.UserCommand;
public class SelectNextTabCommand : IIdentifiableUserCommand
{
public const string CommandName = "next_tab";
public static SelectNextTabCommand Instance { get; } = new();
private SelectNextTabCommand()
{
}
public string UserCommandID => CommandName;
public string Title => "Next tab";
}

View File

@@ -0,0 +1,15 @@
namespace FileTime.App.Core.UserCommand;
public class SelectPreviousTabCommand : IIdentifiableUserCommand
{
public const string CommandName = "previous_tab";
public static SelectPreviousTabCommand Instance { get; } = new();
private SelectPreviousTabCommand()
{
}
public string UserCommandID => CommandName;
public string Title => "New tab";
}

View File

@@ -47,19 +47,15 @@ public class MainConfiguration
private static List<CommandBindingConfiguration> InitDefaultKeyBindings() =>
new List<CommandBindingConfiguration>
{
//new CommandBindingConfiguration(ConfigCommand.AutoRefresh, new KeyConfig(Keys.R, shift: true)),
//new CommandBindingConfiguration(ConfigCommand.ChangeTimelineMode, new[] { Keys.T, Keys.M }),
new(CloseTabCommand.CommandName, Keys.Q),
//new CommandBindingConfiguration(ConfigCommand.Compress, new[] { Keys.Y, Keys.C }),
new(CopyBase64Command.CommandName, new[] {Keys.C, Keys.B}),
new(CopyCommand.CommandName, new[] {Keys.Y, Keys.Y}),
//new CommandBindingConfiguration(ConfigCommand.CopyHash, new[] { Keys.C, Keys.H }),
new(CopyNativePathCommand.CommandName, new[] {Keys.C, Keys.P}),
new(CopyFilesToClipboardCommand.CommandName, new[] {Keys.Y, Keys.C}),
new(CreateContainer.CommandName, Keys.F7),
new(CreateContainer.CommandName, new[] {Keys.C, Keys.C}),
new(CreateElementCommand.CommandName, new[] {Keys.C, Keys.E}),
//new CommandBindingConfiguration(ConfigCommand.Cut, new[] { Keys.D, Keys.D }),
new(EditCommand.CommandName, new[] {Keys.F4}),
new(EnterRapidTravelCommand.CommandName, new KeyConfig(Keys.Comma, shift: true)),
new(EnterRapidTravelCommand.CommandName, new KeyConfig(Keys.Question, shift: true)),
@@ -80,6 +76,7 @@ public class MainConfiguration
new(MoveCursorDownCommand.CommandName, Keys.Down),
new(MoveCursorUpPageCommand.CommandName, Keys.PageUp),
new(MoveCursorDownPageCommand.CommandName, Keys.PageDown),
new(IdentifiableNewTabCommand.CommandName, new[] {new KeyConfig(Keys.T, ctrl: true)}),
//new CommandBindingConfiguration(ConfigCommand.NextTimelineBlock, Keys.L ),
//new CommandBindingConfiguration(ConfigCommand.NextTimelineCommand, Keys.J ),
new(OpenSelectedCommand.CommandName, Keys.Right),
@@ -91,7 +88,6 @@ public class MainConfiguration
new(PasteCommand.PasteSkipCommandName, new[] {Keys.P, Keys.S}),
new(PasteFilesFromClipboardCommand.PasteMergeCommandName, new[] {new KeyConfig(Keys.V, ctrl: true)}),
new(PasteFilesFromClipboardCommand.PasteOverwriteCommandName, new[] {new KeyConfig(Keys.V, ctrl: true, shift: true)}),
//new CommandBindingConfiguration(ConfigCommand.PinFavorite, new[] { Keys.F, Keys.P }),
//new CommandBindingConfiguration(ConfigCommand.PreviousTimelineBlock, Keys.H ),
//new CommandBindingConfiguration(ConfigCommand.PreviousTimelineCommand, Keys.K ),
new(RefreshCommand.CommandName, Keys.R),
@@ -99,10 +95,10 @@ public class MainConfiguration
new(RenameCommand.CommandName, new[] {Keys.C, Keys.W}),
new(IdentifiableRunOrOpenCommand.CommandName, Keys.Enter),
//new CommandBindingConfiguration(ConfigCommand.RunCommand, new KeyConfig(Keys.D4, shift: true)),
//new CommandBindingConfiguration(ConfigCommand.ScanContainerSize, new[] { Keys.C, Keys.S }),
//new CommandBindingConfiguration(ConfigCommand.ShowAllShortcut, Keys.F1),
new(DeleteCommand.SoftDeleteCommandName, new[] {new KeyConfig(Keys.D), new KeyConfig(Keys.D, shift: true)}),
new(IdentifiableSearchCommand.SearchByNameContainsCommandName, new[] {Keys.S, Keys.N}),
new(SelectNextTabCommand.CommandName, new[] {new KeyConfig(Keys.Tab, ctrl: true)}),
new(SelectPreviousTabCommand.CommandName, new[] {new KeyConfig(Keys.Tab, ctrl: true, shift: true)}),
new(SwitchToTabCommand.SwitchToLastTabCommandName, new[] {new KeyConfig(Keys.Num9, alt: true)}),
new(SwitchToTabCommand.SwitchToTab1CommandName, new[] {new KeyConfig(Keys.Num1, alt: true)}),
new(SwitchToTabCommand.SwitchToTab2CommandName, new[] {new KeyConfig(Keys.Num2, alt: true)}),

View File

@@ -36,6 +36,7 @@ public class TabPersistenceService : ITabPersistenceService
private readonly IServiceProvider _serviceProvider;
private readonly ILocalContentProvider _localContentProvider;
private readonly TabPersistenceSettings _tabPersistenceSettings;
private readonly TabsToOpenOnStart _tabsToOpen;
public TabPersistenceService(
IApplicationSettings applicationSettings,
@@ -44,6 +45,7 @@ public class TabPersistenceService : ITabPersistenceService
IServiceProvider serviceProvider,
ILocalContentProvider localContentProvider,
TabPersistenceSettings tabPersistenceSettings,
TabsToOpenOnStart tabsToOpen,
ILogger<TabPersistenceService> logger)
{
_appState = appState;
@@ -53,6 +55,7 @@ public class TabPersistenceService : ITabPersistenceService
_serviceProvider = serviceProvider;
_localContentProvider = localContentProvider;
_tabPersistenceSettings = tabPersistenceSettings;
_tabsToOpen = tabsToOpen;
_jsonOptions = new JsonSerializerOptions
{
@@ -62,7 +65,43 @@ public class TabPersistenceService : ITabPersistenceService
}
public async Task InitAsync()
=> await LoadStatesAsync();
{
var containers = new List<(int? TabNumber, IContainer Container)>();
foreach (var (requestedTabNumber, nativePath) in _tabsToOpen.TabsToOpen)
{
if (await _timelessContentProvider.GetItemByNativePathAsync(nativePath) is not IContainer container) continue;
containers.Add((requestedTabNumber, container));
}
var loadedTabViewModels = await LoadStatesAsync(containers.Count == 0);
var tabViewModels = new List<ITabViewModel>();
foreach (var (requestedTabNumber, container) in containers)
{
var tabNumber = requestedTabNumber ?? 1;
if (tabNumber < 1) tabNumber = 1;
var tabNumbers = _appState.Tabs.Select(t => t.TabNumber).ToHashSet();
while (tabNumbers.Contains(tabNumber))
{
tabNumber++;
}
var tab = await _serviceProvider.GetAsyncInitableResolver(container)
.GetRequiredServiceAsync<ITab>();
var tabViewModel = _serviceProvider.GetInitableResolver(tab, tabNumber).GetRequiredService<ITabViewModel>();
_appState.AddTab(tabViewModel);
tabViewModels.Add(tabViewModel);
}
tabViewModels.Reverse();
await _appState.SetSelectedTabAsync(tabViewModels.Concat(loadedTabViewModels).First());
}
public Task ExitAsync(CancellationToken token = default)
{
@@ -72,12 +111,17 @@ public class TabPersistenceService : ITabPersistenceService
return Task.CompletedTask;
}
private async Task LoadStatesAsync(CancellationToken token = default)
private async Task<IEnumerable<ITabViewModel>> LoadStatesAsync(bool createEmptyIfNecessary, CancellationToken token = default)
{
if (!File.Exists(_settingsPath) || !_tabPersistenceSettings.LoadState)
{
await CreateEmptyTab();
return;
if (createEmptyIfNecessary)
{
var tabViewModel = await CreateEmptyTab();
return new[] {tabViewModel};
}
return Enumerable.Empty<ITabViewModel>();
}
try
@@ -86,7 +130,8 @@ public class TabPersistenceService : ITabPersistenceService
var state = await JsonSerializer.DeserializeAsync<PersistenceRoot>(stateReader, cancellationToken: token);
if (state != null)
{
if (await RestoreTabs(state.TabStates)) return;
var (success, tabViewModels) = await RestoreTabs(state.TabStates);
if (success) return tabViewModels;
}
}
catch (Exception e)
@@ -94,9 +139,15 @@ public class TabPersistenceService : ITabPersistenceService
_logger.LogError(e, "Unknown exception while restoring app state");
}
await CreateEmptyTab();
if (createEmptyIfNecessary)
{
var tabViewModel = await CreateEmptyTab();
return new[] {tabViewModel};
}
async Task CreateEmptyTab()
return Enumerable.Empty<ITabViewModel>();
async Task<ITabViewModel> CreateEmptyTab()
{
IContainer? currentDirectory = null;
try
@@ -116,19 +167,19 @@ public class TabPersistenceService : ITabPersistenceService
var tabViewModel = _serviceProvider.GetInitableResolver(tab, 1).GetRequiredService<ITabViewModel>();
_appState.AddTab(tabViewModel);
return tabViewModel;
}
}
private async Task<bool> RestoreTabs(TabStates? tabStates)
private async Task<(bool Success, IEnumerable<ITabViewModel>)> RestoreTabs(TabStates? tabStates)
{
if (tabStates == null
|| tabStates.Tabs == null)
{
return false;
return (false, Enumerable.Empty<ITabViewModel>());
}
try
{
foreach (var tab in tabStates.Tabs)
{
try
@@ -183,14 +234,8 @@ public class TabPersistenceService : ITabPersistenceService
JsonSerializer.Serialize(tab, _jsonOptions));
}
}
}
catch (Exception e)
{
_logger.LogError(e, "Unknown exception while restoring tabs");
return false;
}
if (_appState.Tabs.Count == 0) return false;
if (_appState.Tabs.Count == 0) return (false, Enumerable.Empty<ITabViewModel>());
var optimalTabs = _appState
.Tabs
@@ -200,10 +245,7 @@ public class TabPersistenceService : ITabPersistenceService
.Tabs
.SkipWhile(t => t.TabNumber <= tabStates.ActiveTabNumber);
var tabToActivate = optimalTabs.Concat(suboptimalTabs).FirstOrDefault();
if (tabToActivate is not null) await _appState.SetSelectedTabAsync(tabToActivate);
return true;
return (true, optimalTabs.Concat(suboptimalTabs));
}
public void SaveStates(CancellationToken token = default)

View File

@@ -86,11 +86,14 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
new TypeUserCommandHandler<MoveCursorToLastCommand>(MoveCursorToLast),
new TypeUserCommandHandler<MoveCursorUpCommand>(MoveCursorUp),
new TypeUserCommandHandler<MoveCursorUpPageCommand>(MoveCursorUpPage),
new TypeUserCommandHandler<NewTabCommand>(NewTabAsync),
new TypeUserCommandHandler<OpenCommandPaletteCommand>(OpenCommandPalette),
new TypeUserCommandHandler<OpenContainerCommand>(OpenContainer),
new TypeUserCommandHandler<OpenSelectedCommand>(OpenSelected),
new TypeUserCommandHandler<RunOrOpenCommand>(RunOrOpen),
new TypeUserCommandHandler<RefreshCommand>(Refresh),
new TypeUserCommandHandler<SelectNextTabCommand>(SelectNextTab),
new TypeUserCommandHandler<SelectPreviousTabCommand>(SelectPreviousTab),
new TypeUserCommandHandler<SwitchToTabCommand>(SwitchToTab),
});
}
@@ -346,38 +349,19 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
private async Task SwitchToTab(SwitchToTabCommand command)
{
var number = command.TabNumber;
var tabViewModel = _appState.Tabs.FirstOrDefault(t => t.TabNumber == number);
var tabNumber = command.TabNumber;
var tabViewModel = _appState.Tabs.FirstOrDefault(t => t.TabNumber == tabNumber);
if (number == -1)
if (tabNumber == -1)
{
var greatestNumber = _appState.Tabs.Max(t => t.TabNumber);
tabViewModel = _appState.Tabs.FirstOrDefault(t => t.TabNumber == greatestNumber);
}
else if (tabViewModel == null)
if (tabViewModel == null)
{
IContainer? newLocation = null;
try
{
newLocation = _currentLocation?.Value?.FullName is { } fullName
? (IContainer) await _timelessContentProvider.GetItemByFullNameAsync(fullName, PointInTime.Present)
: _localContentProvider;
}
catch (Exception ex)
{
var fullName = _currentLocation?.Value?.FullName?.Path ?? "unknown";
_logger.LogError(ex, "Could not resolve container while switching to tab {TabNumber} to path {FullName}", number, fullName);
}
newLocation ??= _localContentProvider;
var tab = await _serviceProvider.GetAsyncInitableResolver(newLocation)
.GetRequiredServiceAsync<ITab>();
var newTabViewModel = _serviceProvider.GetInitableResolver(tab, number).GetRequiredService<ITabViewModel>();
_appState.AddTab(newTabViewModel);
tabViewModel = newTabViewModel;
var newLocation = await GetLocationForNewTabAsync(tabNumber);
tabViewModel = await CreateTabAsync(newLocation, tabNumber);
}
if (_viewMode == ViewMode.RapidTravel)
@@ -385,9 +369,96 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
await _userCommandHandlerService.HandleCommandAsync(ExitRapidTravelCommand.Instance);
}
await _appState.SetSelectedTabAsync(tabViewModel);
}
private async Task NewTabAsync(NewTabCommand command)
{
var numbers = _appState.Tabs.Select(t => t.TabNumber).ToHashSet();
var tabNumber = 1;
while (numbers.Contains(tabNumber))
{
tabNumber++;
}
IContainer? newLocation = null;
if (command.Path is { } path)
{
newLocation = await _timelessContentProvider.GetItemByFullNameAsync(path, PointInTime.Present) as IContainer;
}
newLocation ??= await GetLocationForNewTabAsync(tabNumber);
var tabViewModel = await CreateTabAsync(newLocation, tabNumber);
if (_viewMode == ViewMode.RapidTravel)
{
await _userCommandHandlerService.HandleCommandAsync(ExitRapidTravelCommand.Instance);
}
if (command.Open)
{
await _appState.SetSelectedTabAsync(tabViewModel);
}
}
private async Task SelectNextTab()
{
var currentTabNumber = _appState.SelectedTab.Value?.TabNumber;
var nextTabNumbers = _appState.Tabs.Select(t => t.TabNumber).Order().SkipWhile(n => n <= currentTabNumber).ToArray();
if (nextTabNumbers.Length == 0) return;
var nextTabNumber = nextTabNumbers[0];
var tabViewModel = _appState.Tabs.FirstOrDefault(t => t.TabNumber == nextTabNumber);
await _appState.SetSelectedTabAsync(tabViewModel!);
}
private async Task SelectPreviousTab()
{
var currentTabNumber = _appState.SelectedTab.Value?.TabNumber;
var nextTabNumbers = _appState.Tabs.Select(t => t.TabNumber).Order().TakeWhile(n => n < currentTabNumber).ToArray();
if (nextTabNumbers.Length == 0) return;
var nextTabNumber = nextTabNumbers[^1];
var tabViewModel = _appState.Tabs.FirstOrDefault(t => t.TabNumber == nextTabNumber);
await _appState.SetSelectedTabAsync(tabViewModel!);
}
private async Task<IContainer> GetLocationForNewTabAsync(int tabNumber)
{
try
{
var newLocation = _currentLocation?.Value?.FullName is { } fullName
? (IContainer) await _timelessContentProvider.GetItemByFullNameAsync(fullName, PointInTime.Present)
: _localContentProvider;
return newLocation;
}
catch (Exception ex)
{
var fullName = _currentLocation?.Value?.FullName?.Path ?? "unknown";
_logger.LogError(ex, "Could not resolve container while switching to tab {TabNumber} to path {FullName}", tabNumber, fullName);
}
return _localContentProvider;
}
private async Task<ITabViewModel> CreateTabAsync(IContainer newLocation, int tabNumber)
{
var tab = await _serviceProvider.GetAsyncInitableResolver(newLocation)
.GetRequiredServiceAsync<ITab>();
var newTabViewModel = _serviceProvider.GetInitableResolver(tab, tabNumber).GetRequiredService<ITabViewModel>();
_appState.AddTab(newTabViewModel);
return newTabViewModel;
}
private Task CloseTab()
{
if ((!_applicationConfiguration.AllowCloseLastTab && _appState.Tabs.Count < 2) || _selectedTab == null) return Task.CompletedTask;

View File

@@ -1,4 +1,5 @@
using FileTime.App.Core.Configuration;
using FileTime.App.Core.Models;
using FileTime.App.Core.Services;
using FileTime.App.Core.Services.UserCommandHandler;
using FileTime.App.Core.StartupServices;
@@ -36,6 +37,7 @@ public static class Startup
serviceCollection.TryAddSingleton<IPossibleCommandsService, PossibleCommandsService>();
serviceCollection.TryAddSingleton<IPossibleCommandsViewModel, PossibleCommandsViewModel>();
serviceCollection.TryAddSingleton<IProgramsService, ProgramsService>();
serviceCollection.TryAddSingleton(new TabsToOpenOnStart(new List<TabToOpen>()));
return serviceCollection
.AddCommandHandlers()

View File

@@ -13,8 +13,8 @@ public class DefaultIdentifiableCommandHandlerRegister : IStartupHandler
AddUserCommand(AddRemoteContentProviderCommand.Instance);
AddUserCommand(CloseTabCommand.Instance);
AddUserCommand(CopyCommand.Instance);
AddUserCommand(CopyBase64Command.Instance);
AddUserCommand(CopyCommand.Instance);
AddUserCommand(CopyFilesToClipboardCommand.Instance);
AddUserCommand(CopyNativePathCommand.Instance);
AddUserCommand(CreateContainer.Instance);
@@ -32,6 +32,9 @@ public class DefaultIdentifiableCommandHandlerRegister : IStartupHandler
AddUserCommand(GoToProviderCommand.Instance);
AddUserCommand(GoToRootCommand.Instance);
AddUserCommand(GoUpCommand.Instance);
AddUserCommand(IdentifiableRunOrOpenCommand.Instance);
AddUserCommand(IdentifiableSearchCommand.SearchByNameContains);
AddUserCommand(IdentifiableSearchCommand.SearchByRegex);
AddUserCommand(MarkCommand.Instance);
AddUserCommand(MoveCursorDownCommand.Instance);
AddUserCommand(MoveCursorDownPageCommand.Instance);
@@ -39,6 +42,7 @@ public class DefaultIdentifiableCommandHandlerRegister : IStartupHandler
AddUserCommand(MoveCursorToLastCommand.Instance);
AddUserCommand(MoveCursorUpCommand.Instance);
AddUserCommand(MoveCursorUpPageCommand.Instance);
AddUserCommand(IdentifiableNewTabCommand.Instance);
AddUserCommand(OpenCommandPaletteCommand.Instance);
AddUserCommand(OpenInDefaultFileExplorerCommand.Instance);
AddUserCommand(OpenSelectedCommand.Instance);
@@ -51,19 +55,18 @@ public class DefaultIdentifiableCommandHandlerRegister : IStartupHandler
AddUserCommand(PauseCommandSchedulerCommand.Instance);
AddUserCommand(RefreshCommand.Instance);
AddUserCommand(RenameCommand.Instance);
AddUserCommand(IdentifiableRunOrOpenCommand.Instance);
AddUserCommand(ScanSizeCommand.Instance);
AddUserCommand(StartCommandSchedulerCommand.Instance);
AddUserCommand(SortItemsCommand.OrderByNameCommand);
AddUserCommand(SortItemsCommand.OrderByNameDescCommand);
AddUserCommand(SelectNextTabCommand.Instance);
AddUserCommand(SelectPreviousTabCommand.Instance);
AddUserCommand(SortItemsCommand.OrderByCreatedAtCommand);
AddUserCommand(SortItemsCommand.OrderByCreatedAtDescCommand);
AddUserCommand(SortItemsCommand.OrderByLastModifiedCommand);
AddUserCommand(SortItemsCommand.OrderByLastModifiedDescCommand);
AddUserCommand(SortItemsCommand.OrderByNameCommand);
AddUserCommand(SortItemsCommand.OrderByNameDescCommand);
AddUserCommand(SortItemsCommand.OrderBySizeCommand);
AddUserCommand(SortItemsCommand.OrderBySizeDescCommand);
AddUserCommand(IdentifiableSearchCommand.SearchByNameContains);
AddUserCommand(IdentifiableSearchCommand.SearchByRegex);
AddUserCommand(StartCommandSchedulerCommand.Instance);
AddUserCommand(SwitchToTabCommand.SwitchToLastTab);
AddUserCommand(SwitchToTabCommand.SwitchToTab1);
AddUserCommand(SwitchToTabCommand.SwitchToTab2);

View File

@@ -25,12 +25,21 @@
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.4" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.4" />
<PackageReference Include="Avalonia.Svg.Skia" Version="11.0.0" />
<PackageReference Include="Avalonia.Svg.Skia" Version="11.0.0.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.4" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.2" />
<PackageReference Include="MessagePack" Version="2.5.124" />
<PackageReference Include="MessagePackAnalyzer" Version="2.5.124">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="MvvmGen" Version="1.2.1" />
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Syroot.Windows.IO.KnownFolders" Version="1.3.0" />
@@ -48,4 +57,7 @@
<ProjectReference Include="..\FileTime.GuiApp.Font.Abstractions\FileTime.GuiApp.Font.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
using FileTime.GuiApp.App.InstanceManagement.Messages;
namespace FileTime.GuiApp.App.InstanceManagement;
public class DummyInstanceMessageHandler : IInstanceMessageHandler
{
public Task HandleMessageAsync(IInstanceMessage message) => throw new NotImplementedException();
public event Action? ShowWindow;
}

View File

@@ -0,0 +1,10 @@
using FileTime.App.Core.Services;
using FileTime.GuiApp.App.InstanceManagement.Messages;
namespace FileTime.GuiApp.App.InstanceManagement;
public interface IInstanceManager : IStartupHandler, IExitHandler
{
Task SendMessageAsync<T>(T message, CancellationToken token = default) where T : class, IInstanceMessage;
Task<bool> TryConnectAsync(CancellationToken token = default);
}

View File

@@ -0,0 +1,9 @@
using FileTime.GuiApp.App.InstanceManagement.Messages;
namespace FileTime.GuiApp.App.InstanceManagement;
public interface IInstanceMessageHandler
{
Task HandleMessageAsync(IInstanceMessage message);
event Action? ShowWindow;
}

View File

@@ -0,0 +1,178 @@
using System.IO.Pipes;
using Avalonia.Controls;
using FileTime.GuiApp.App.InstanceManagement.Messages;
using MessagePack;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace FileTime.GuiApp.App.InstanceManagement;
public sealed class InstanceManager : IInstanceManager
{
private const string PipeName = "FileTime.GuiApp";
private readonly IInstanceMessageHandler _instanceMessageHandler;
private readonly ILogger _logger;
private readonly CancellationTokenSource _serverCancellationTokenSource = new();
private Thread? _serverThread;
private NamedPipeClientStream? _pipeClientStream;
private readonly List<NamedPipeServerStream> _pipeServerStreams = new();
[ActivatorUtilitiesConstructor]
public InstanceManager(
IInstanceMessageHandler instanceMessageHandler,
ILogger<InstanceManager> logger)
{
_instanceMessageHandler = instanceMessageHandler;
_logger = logger;
}
public InstanceManager(
IInstanceMessageHandler instanceMessageHandler,
ILogger logger)
{
_instanceMessageHandler = instanceMessageHandler;
_logger = logger;
}
public async Task<bool> TryConnectAsync(CancellationToken token = default)
{
if (_pipeClientStream is not null) return true;
_pipeClientStream = new NamedPipeClientStream(".", PipeName, PipeDirection.InOut);
try
{
await _pipeClientStream.ConnectAsync(200, token);
}
catch
{
return false;
}
return true;
}
public Task ExitAsync(CancellationToken token = default)
{
_serverCancellationTokenSource.Cancel();
var pipeServerStreams = _pipeServerStreams.ToArray();
_pipeServerStreams.Clear();
foreach (var pipeServerStream in pipeServerStreams)
{
try
{
pipeServerStream.Close();
pipeServerStream.Dispose();
}
catch
{
// ignored
}
}
return Task.CompletedTask;
}
public Task InitAsync()
{
_serverThread = new Thread(StartServer);
_serverThread.Start();
return Task.CompletedTask;
}
private async void StartServer()
{
try
{
if (await TryConnectAsync())
{
//An instance already exists, this one won't listen for connections
return;
}
while (true)
{
var pipeServer = new NamedPipeServerStream(PipeName, PipeDirection.InOut, 200);
_pipeServerStreams.Add(pipeServer);
try
{
await pipeServer.WaitForConnectionAsync(_serverCancellationTokenSource.Token);
ThreadPool.QueueUserWorkItem(HandleConnection, pipeServer);
}
catch (OperationCanceledException)
{
break;
}
}
}
catch (Exception e)
{
_logger.LogError(e, "Error in server thread");
}
}
private async void HandleConnection(object? state)
{
if (state is not NamedPipeServerStream pipeServer)
throw new ArgumentException(nameof(state) + "is not" + nameof(NamedPipeServerStream));
while (true)
{
IInstanceMessage message;
try
{
message = await ReadMessageAsync(pipeServer, _serverCancellationTokenSource.Token);
}
catch (TaskCanceledException)
{
break;
}
catch (MessagePackSerializationException)
{
break;
}
catch (Exception e)
{
_logger.LogError(e, "Error while reading message");
break;
}
try
{
await _instanceMessageHandler.HandleMessageAsync(message);
}
catch (Exception e)
{
_logger.LogError(e, "Error while handling message");
break;
}
}
_pipeServerStreams.Remove(pipeServer);
pipeServer.Close();
await pipeServer.DisposeAsync();
}
public async Task SendMessageAsync<T>(T message, CancellationToken token = default) where T : class, IInstanceMessage
{
if (!await TryConnectAsync(token))
{
_logger.LogWarning("Could not connect to server, can send message {Message}", message);
return;
}
await WriteMessageAsync(_pipeClientStream!, message, token);
await _pipeClientStream!.FlushAsync(token);
}
private static async Task<IInstanceMessage> ReadMessageAsync(Stream stream, CancellationToken token = default)
=> await MessagePackSerializer.DeserializeAsync<IInstanceMessage>(stream, cancellationToken: token);
private static async Task WriteMessageAsync(Stream stream, IInstanceMessage message, CancellationToken token = default)
=> await MessagePackSerializer.SerializeAsync(stream, message, cancellationToken: token);
}

View File

@@ -0,0 +1,41 @@
using FileTime.App.Core.Services;
using FileTime.App.Core.UserCommand;
using FileTime.Core.Models;
using FileTime.Core.Timeline;
using FileTime.GuiApp.App.InstanceManagement.Messages;
namespace FileTime.GuiApp.App.InstanceManagement;
public class InstanceMessageHandler : IInstanceMessageHandler
{
private readonly IUserCommandHandlerService _userCommandHandlerService;
private readonly ITimelessContentProvider _timelessContentProvider;
public event Action? ShowWindow;
public InstanceMessageHandler(
IUserCommandHandlerService userCommandHandlerService,
ITimelessContentProvider timelessContentProvider
)
{
_userCommandHandlerService = userCommandHandlerService;
_timelessContentProvider = timelessContentProvider;
}
public async Task HandleMessageAsync(IInstanceMessage message)
{
if (message is OpenContainers openContainers)
{
foreach (var container in openContainers.Containers)
{
var fullName = await _timelessContentProvider.GetFullNameByNativePathAsync(new NativePath(container));
if (fullName is null) continue;
await _userCommandHandlerService.HandleCommandAsync(new NewTabCommand(fullName));
}
ShowWindow?.Invoke();
}
}
}

View File

@@ -0,0 +1,9 @@

namespace FileTime.GuiApp.App.InstanceManagement.Messages;
[MessagePack.Union(0, typeof(OpenContainers))]
public interface IInstanceMessage
{
}

View File

@@ -0,0 +1,20 @@
using MessagePack;
namespace FileTime.GuiApp.App.InstanceManagement.Messages;
[MessagePackObject]
public class OpenContainers : IInstanceMessage
{
public OpenContainers()
{
}
public OpenContainers(IEnumerable<string> containers)
{
Containers.AddRange(containers);
}
[Key(0)]
public List<string> Containers { get; set; } = new();
}

View File

@@ -71,10 +71,15 @@ public class SystemClipboardService : ISystemClipboardService
return;
}
var fileNativePaths = files
.Select(i => _timelessContentProvider.GetNativePathByFullNameAsync(i))
.Where(i => i != null)
.OfType<NativePath>();
var fileNativePaths = new List<NativePath>();
foreach (var file in files)
{
var fileNativePath = await _timelessContentProvider.GetNativePathByFullNameAsync(file);
if (fileNativePath is not null)
{
fileNativePaths.Add(fileNativePath);
}
}
var targetFiles = new List<IStorageFile>();
foreach (var fileNativePath in fileNativePaths)

View File

@@ -0,0 +1,239 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
namespace FileTime.GuiApp.App.Settings;
public class DefaultBrowserRegister : IDefaultBrowserRegister
{
private const string WinEKeyPathSub = @"shell\opennewwindow\command";
private const string WinEKeyPathRoot = @"SOFTWARE\Classes\CLSID\{52205fd8-5dfb-447d-801a-d0b52f2e83e1}";
private const string WinEKeyPath = $@"{WinEKeyPathRoot}\{WinEKeyPathSub}";
private const string FullWinEKeyPath = $@"HKEY_CURRENT_USER\{WinEKeyPath}";
private const string FullWinEKeyPathRoot = $@"HKEY_CURRENT_USER\{WinEKeyPathRoot}";
private const string DefaultBrowserKeyPath = @"SOFTWARE\Classes\Folder\shell\open\command";
private const string FullDefaultBrowserKeyPath = $@"HKEY_CURRENT_USER\{DefaultBrowserKeyPath}";
private readonly ILogger<DefaultBrowserRegister> _logger;
public DefaultBrowserRegister(ILogger<DefaultBrowserRegister> logger)
{
_logger = logger;
}
public async void RegisterAsDefaultEditor()
{
string? tempFile = null;
try
{
tempFile = Path.GetTempFileName() + ".reg";
var hexPath = GetFileTimeHexPath("\"%1\"");
await using (var streamWriter = new StreamWriter(tempFile))
{
await streamWriter.WriteLineAsync("Windows Registry Editor Version 5.00");
await streamWriter.WriteLineAsync();
var s = $$"""
[{{FullDefaultBrowserKeyPath}}]
@={{hexPath}}
"DelegateExecute"=""
[HKEY_CURRENT_USER\SOFTWARE\Classes\Folder\shell\explore\command]
@={{hexPath}}
"DelegateExecute"=""
""";
await streamWriter.WriteLineAsync(s);
await streamWriter.FlushAsync();
}
await StartRegeditProcessAsync(tempFile);
}
catch (Exception e)
{
_logger.LogError(e, "Error while registering Win+E shortcut");
}
finally
{
if (tempFile is not null)
{
File.Delete(tempFile);
}
}
}
public async void UnregisterAsDefaultEditor()
{
string? tempFile = null;
try
{
tempFile = Path.GetTempFileName() + ".reg";
await using (var streamWriter = new StreamWriter(tempFile))
{
await streamWriter.WriteLineAsync("Windows Registry Editor Version 5.00");
await streamWriter.WriteLineAsync();
var s = $$"""
[HKEY_CURRENT_USER\SOFTWARE\Classes\Folder\shell\open]
"MultiSelectModel"="Document"
[{{FullDefaultBrowserKeyPath}}]
@=hex(2):25,00,53,00,79,00,73,00,74,00,65,00,6d,00,52,00,6f,00,6f,00,74,00,25,\
00,5c,00,45,00,78,00,70,00,6c,00,6f,00,72,00,65,00,72,00,2e,00,65,00,78,00,\
65,00,00,00
"DelegateExecute"="{11dbb47c-a525-400b-9e80-a54615a090c0}"
[HKEY_CURRENT_USER\SOFTWARE\Classes\Folder\shell\explore]
"LaunchExplorerFlags"=dword:00000018
"MultiSelectModel"="Document"
"ProgrammaticAccessOnly"=""
[HKEY_CURRENT_USER\SOFTWARE\Classes\Folder\shell\explore\command]
@=-
"DelegateExecute"="{11dbb47c-a525-400b-9e80-a54615a090c0}"
""";
await streamWriter.WriteLineAsync(s);
await streamWriter.FlushAsync();
}
await StartRegeditProcessAsync(tempFile);
}
catch (Exception e)
{
_logger.LogError(e, "Error while registering Win+E shortcut");
}
finally
{
if (tempFile is not null)
{
File.Delete(tempFile);
}
}
}
public async void RegisterWinEShortcut()
{
string? tempFile = null;
try
{
tempFile = Path.GetTempFileName() + ".reg";
var hexPath = GetFileTimeHexPath();
await using (var streamWriter = new StreamWriter(tempFile))
{
await streamWriter.WriteLineAsync("Windows Registry Editor Version 5.00");
await streamWriter.WriteLineAsync();
var s = $$"""
[{{FullWinEKeyPath}}]
@={{hexPath}}
"DelegateExecute"=""
""";
await streamWriter.WriteLineAsync(s);
await streamWriter.FlushAsync();
}
await StartRegeditProcessAsync(tempFile);
}
catch (Exception e)
{
_logger.LogError(e, "Error while registering Win+E shortcut");
}
finally
{
if (tempFile is not null)
{
File.Delete(tempFile);
}
}
}
public async void UnregisterWinEShortcut()
{
string? tempFile = null;
try
{
tempFile = Path.GetTempFileName() + ".reg";
await using (var streamWriter = new StreamWriter(tempFile))
{
await streamWriter.WriteLineAsync("Windows Registry Editor Version 5.00");
await streamWriter.WriteLineAsync();
var s = $"[-{FullWinEKeyPathRoot}]";
await streamWriter.WriteLineAsync(s);
await streamWriter.FlushAsync();
}
await StartRegeditProcessAsync(tempFile);
}
catch (Exception e)
{
_logger.LogError(e, "Error while unregistering Win+E shortcut");
}
finally
{
if (tempFile is not null)
{
File.Delete(tempFile);
}
}
}
public bool IsWinEShortcut() => IsKeyContainsPath(WinEKeyPath);
public bool IsDefaultFileBrowser() => IsKeyContainsPath(DefaultBrowserKeyPath);
private bool IsKeyContainsPath(string key)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return false;
using var subKey = Registry.CurrentUser.OpenSubKey(key);
var command = (string?) subKey?.GetValue(string.Empty);
return !string.IsNullOrEmpty(command) && command.Contains(GetFileTimeExecutablePath());
}
private string GetFileTimeHexPath(string? parameter = null)
{
var fileTimePath = GetFileTimeExecutablePath();
var hexPath = GetRegistryHexString("\"" + fileTimePath + "\"" + (parameter is null ? "" : " " + parameter));
return hexPath;
}
private async Task StartRegeditProcessAsync(string regFilePath)
{
try
{
using var regProcess = Process.Start(
new ProcessStartInfo(
"regedit.exe",
@$"/s ""{regFilePath}""")
{
UseShellExecute = true,
Verb = "runas"
});
if (regProcess is not null)
{
await regProcess.WaitForExitAsync();
}
}
catch
{
// Canceled UAC
}
}
private string GetFileTimeExecutablePath()
=> Process.GetCurrentProcess().MainModule!.FileName;
private string GetRegistryHexString(string s)
{
var bytes = Encoding.Unicode.GetBytes(s);
return "hex(2):" + BitConverter.ToString(bytes).Replace("-", ",");
}
}

View File

@@ -0,0 +1,12 @@
namespace FileTime.GuiApp.App.Settings;
public interface IDefaultBrowserRegister
{
void RegisterAsDefaultEditor();
void UnregisterAsDefaultEditor();
void RegisterWinEShortcut();
void UnregisterWinEShortcut();
bool IsWinEShortcut();
bool IsDefaultFileBrowser();
}

View File

@@ -0,0 +1,10 @@
namespace FileTime.GuiApp.App.Settings;
public interface ISettingsViewModel
{
SettingsPane SelectedPane { get; set; }
bool SetAsDefaultIsChecked { get; set; }
List<SettingsPaneItem> PaneItems { get; }
SettingsPaneItem SelectedPaneItem { get; set; }
bool SetAsWinEHandlerIsChecked { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace FileTime.GuiApp.App.Settings;
public enum SettingsPane
{
Home,
Advanced
}

View File

@@ -0,0 +1,66 @@
using System.ComponentModel;
using System.Runtime.InteropServices;
using PropertyChanged.SourceGenerator;
namespace FileTime.GuiApp.App.Settings;
public record SettingsPaneItem(string Header, SettingsPane Pane);
public partial class SettingsViewModel : ISettingsViewModel
{
private readonly IDefaultBrowserRegister _defaultBrowserRegister;
[Notify] private SettingsPane _selectedPane;
[Notify] private bool _setAsDefaultIsChecked;
[Notify] private bool _setAsWinEHandlerIsChecked;
public bool ShowWindowsSpecificSettings => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public List<SettingsPaneItem> PaneItems { get; } = new()
{
new("Home", SettingsPane.Home),
new("Advanced", SettingsPane.Advanced)
};
public SettingsPaneItem SelectedPaneItem
{
get => PaneItems.First(x => x.Pane == SelectedPane);
set => SelectedPane = value.Pane;
}
public SettingsViewModel(IDefaultBrowserRegister defaultBrowserRegister)
{
_defaultBrowserRegister = defaultBrowserRegister;
_setAsWinEHandlerIsChecked = defaultBrowserRegister.IsWinEShortcut();
_setAsDefaultIsChecked = defaultBrowserRegister.IsDefaultFileBrowser();
PropertyChanged += SettingsViewModel_PropertyChanged;
}
private void SettingsViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SetAsDefaultIsChecked))
{
if (SetAsDefaultIsChecked)
{
_defaultBrowserRegister.RegisterAsDefaultEditor();
}
else
{
_defaultBrowserRegister.UnregisterAsDefaultEditor();
}
}
else if (e.PropertyName == nameof(SetAsWinEHandlerIsChecked))
{
if (SetAsWinEHandlerIsChecked)
{
_defaultBrowserRegister.RegisterWinEShortcut();
}
else
{
_defaultBrowserRegister.UnregisterWinEShortcut();
}
}
}
}

View File

@@ -20,5 +20,6 @@ public interface IMainWindowViewModel : IMainWindowViewModelBase
IClipboardService ClipboardService { get; }
ITimelineViewModel TimelineViewModel { get; }
IPossibleCommandsViewModel PossibleCommands { get; }
Action? ShowWindow { get; set; }
Task RunOrOpenItem(IItemViewModel itemViewModel);
}

View File

@@ -11,6 +11,7 @@ using FileTime.App.Core.ViewModels.Timeline;
using FileTime.App.FrequencyNavigation.Services;
using FileTime.Core.Models;
using FileTime.Core.Timeline;
using FileTime.GuiApp.App.InstanceManagement;
using FileTime.GuiApp.App.Services;
using FileTime.Providers.Local;
using FileTime.Providers.LocalAdmin;
@@ -39,6 +40,7 @@ namespace FileTime.GuiApp.App.ViewModels;
[Inject(typeof(IModalService), PropertyName = "_modalService")]
[Inject(typeof(ITimelineViewModel), PropertyAccessModifier = AccessModifier.Public)]
[Inject(typeof(IPossibleCommandsViewModel), PropertyName = "PossibleCommands", PropertyAccessModifier = AccessModifier.Public)]
[Inject(typeof(IInstanceMessageHandler), PropertyName = "_instanceMessageHandler")]
public partial class MainWindowViewModel : IMainWindowViewModel
{
public bool Loading => false;
@@ -48,6 +50,7 @@ public partial class MainWindowViewModel : IMainWindowViewModel
public IGuiAppState AppState => _appState;
public DeclarativeProperty<string> Title { get; } = new();
public Action? FocusDefaultElement { get; set; }
public Action? ShowWindow { get; set; }
partial void OnInitialize()
{
@@ -72,6 +75,7 @@ public partial class MainWindowViewModel : IMainWindowViewModel
Title.SetValueSafe(title);
_modalService.AllModalClosed += (_, _) => FocusDefaultElement?.Invoke();
_instanceMessageHandler.ShowWindow += () => ShowWindow?.Invoke();
Task.Run(async () => await _lifecycleService.InitStartupHandlersAsync()).Wait();
}

View File

@@ -1,12 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Avalonia.VisualTree;
using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels;
using FileTime.Core.Models;
using FileTime.GuiApp.App.Services;
using FileTime.GuiApp.App.Settings;
using FileTime.GuiApp.App.ViewModels;
using FileTime.Providers.Local;
using Microsoft.Extensions.DependencyInjection;
@@ -68,6 +70,7 @@ public partial class MainWindow : Window, IUiAccessor
{
var viewModel = DI.ServiceProvider.GetRequiredService<MainWindowViewModel>();
viewModel.FocusDefaultElement = () => Focus();
viewModel.ShowWindow = Activate;
ViewModel = viewModel;
}
catch (Exception ex)
@@ -128,17 +131,6 @@ public partial class MainWindow : Window, IUiAccessor
{
path = placeInfoPath;
}
/*else if (control.DataContext is IElement element && element.GetParent() is IContainer parentContainer)
{
Task.Run(async () =>
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
await ViewModel.AppState.SelectedTab.OpenContainer(parentContainer);
await ViewModel.AppState.SelectedTab.SetCurrentSelectedItem(element);
});
});
}*/
if (path is null) return;
@@ -253,4 +245,13 @@ public partial class MainWindow : Window, IUiAccessor
});
}
}
private void SettingsButton_OnClick(object? sender, RoutedEventArgs e)
{
var settingsWindow = new SettingsWindow
{
DataContext = DI.ServiceProvider.GetRequiredService<ISettingsViewModel>()
};
settingsWindow.ShowDialog(this);
}
}

View File

@@ -0,0 +1,43 @@
<Window
Background="{DynamicResource AppBackgroundColor}"
Height="500"
Title="Settings"
Width="600"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d"
x:Class="FileTime.GuiApp.App.Views.SettingsWindow"
x:CompileBindings="True"
x:DataType="settings:ISettingsViewModel"
xmlns="https://github.com/avaloniaui"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:settings="clr-namespace:FileTime.GuiApp.App.Settings"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid RowDefinitions="Auto * Auto">
<TabStrip
HorizontalAlignment="Center"
ItemsSource="{Binding PaneItems}"
SelectedItem="{Binding SelectedPaneItem}">
<TabStrip.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabStrip.ItemTemplate>
</TabStrip>
<Grid Grid.Row="1" Margin="10">
<StackPanel IsVisible="{Binding SelectedPane, Converter={StaticResource EqualsConverter}, ConverterParameter={x:Static settings:SettingsPane.Advanced}}" VerticalAlignment="Top">
<ToggleSwitch IsChecked="{Binding SetAsDefaultIsChecked, Mode=TwoWay}">
<ToggleSwitch.OnContent>Set as default file browser</ToggleSwitch.OnContent>
<ToggleSwitch.OffContent>Set as default file browser</ToggleSwitch.OffContent>
</ToggleSwitch>
<ToggleSwitch IsChecked="{Binding SetAsWinEHandlerIsChecked, Mode=TwoWay}">
<ToggleSwitch.OnContent>Set Win+E shortcut</ToggleSwitch.OnContent>
<ToggleSwitch.OffContent>Set Win+E shortcut</ToggleSwitch.OffContent>
</ToggleSwitch>
</StackPanel>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace FileTime.GuiApp.App.Views;
public partial class SettingsWindow : Window
{
public SettingsWindow()
{
InitializeComponent();
}
}

View File

@@ -1,10 +1,13 @@
using System.Linq;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using FileTime.App.CommandPalette;
using FileTime.App.ContainerSizeScanner;
using FileTime.App.Core.Models;
using FileTime.App.DependencyInjection;
using FileTime.App.FrequencyNavigation;
using FileTime.App.Search;
using FileTime.Core.Models;
using FileTime.GuiApp.App;
using FileTime.GuiApp.App.Font;
using FileTime.GuiApp.App.ViewModels;
@@ -21,7 +24,7 @@ public class Application : Avalonia.Application
private static void InitializeApp()
{
var configuration = Startup.CreateConfiguration();
DI.ServiceProvider = DependencyInjection
var services = DependencyInjection
.RegisterDefaultServices(configuration: configuration)
.AddServerCoreServices()
.AddFrequencyNavigation()
@@ -33,8 +36,21 @@ public class Application : Avalonia.Application
.RegisterLogging()
.RegisterGuiServices()
.AddSettings()
.AddViewModels()
.BuildServiceProvider();
.AddViewModels();
if (Program.DirectoriesToOpen.Count > 0)
{
services.AddSingleton(
new TabsToOpenOnStart(
Program
.DirectoriesToOpen
.Select(d => new TabToOpen(null, new NativePath(d)))
.ToList()
)
);
}
DI.ServiceProvider = services.BuildServiceProvider();
var logger = DI.ServiceProvider.GetRequiredService<ILogger<Application>>();
logger.LogInformation("App initialization completed");

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
@@ -7,15 +8,22 @@ using System.Threading.Tasks;
using Avalonia;
using Avalonia.ReactiveUI;
using FileTime.App.Core;
using FileTime.GuiApp.App.InstanceManagement;
using FileTime.GuiApp.App.InstanceManagement.Messages;
using Serilog;
using Serilog.Debugging;
using Serilog.Extensions.Logging;
namespace FileTime.GuiApp;
public static class Program
{
public static string AppDataRoot { get; private set; }
public static string EnvironmentName { get; private set; }
public static string AppDataRoot { get; private set; } = null!;
public static string EnvironmentName { get; private set; } = null!;
private static ILogger _logger = null!;
internal static List<string> DirectoriesToOpen { get; } = new();
private static void InitLogging()
{
@@ -37,13 +45,15 @@ public static class Program
rollingInterval: RollingInterval.Day,
rollOnFileSizeLimit: true)
.CreateBootstrapLogger();
_logger = Log.ForContext(typeof(Program));
}
// 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)
public static async Task Main(string[] args)
{
#if DEBUG
(AppDataRoot, EnvironmentName) = Init.InitDevelopment();
@@ -55,8 +65,53 @@ public static class Program
InitLogging();
Log.Logger.Information("Early app starting...");
_logger.Information("Early app starting...");
_logger.Information("Args ({ArgsLength}): {Args}", args.Length, $"\"{string.Join("\", \"", args)}\"");
if (!await CheckDirectoryArguments(args))
{
NormalStartup(args);
}
}
private static async Task<bool> CheckDirectoryArguments(string[] args)
{
var directoryArguments = new List<string>();
foreach (var path in args)
{
var directory = new DirectoryInfo(path);
if (directory.Exists)
{
directoryArguments.Add(directory.FullName);
}
}
if (directoryArguments.Count == 0) return false;
var loggerFactory = new SerilogLoggerFactory(Log.Logger);
var logger = loggerFactory.CreateLogger(typeof(InstanceManager).Name);
var instanceManager = new InstanceManager(new DummyInstanceMessageHandler(), logger);
try
{
if (await instanceManager.TryConnectAsync())
{
await instanceManager.SendMessageAsync(new OpenContainers(directoryArguments));
return true;
}
}
catch
{
// ignored
}
DirectoriesToOpen.AddRange(directoryArguments);
return false;
}
private static void NormalStartup(string[] args)
{
AppDomain.CurrentDomain.FirstChanceException += OnFirstChanceException;
AppDomain.CurrentDomain.UnhandledException += OnAppDomainUnhandledException;
TaskScheduler.UnobservedTaskException += OnTaskSchedulerUnobservedTaskException;
@@ -73,6 +128,7 @@ public static class Program
{
Log.CloseAndFlush();
}
}
// Avalonia configuration, don't remove; also used by visual designer.
@@ -91,8 +147,9 @@ public static class Program
private static void OnFirstChanceException(object? sender, FirstChanceExceptionEventArgs e)
=> HandleUnhandledException(sender, e.Exception);
[Conditional("DEBUG")]
private static void HandleUnhandledException(object? sender, Exception? ex, [CallerMemberName] string caller = "")
=> Log.Warning(
=> _logger.Debug(
ex,
"An unhandled exception come from '{Caller}' exception handler from an object of type '{Type}' and value '{Value}': {Exception}",
caller,

View File

@@ -7,8 +7,10 @@ using FileTime.App.Core.ViewModels;
using FileTime.Core.Interactions;
using FileTime.GuiApp.CustomImpl.ViewModels;
using FileTime.GuiApp.App.IconProviders;
using FileTime.GuiApp.App.InstanceManagement;
using FileTime.GuiApp.App.Logging;
using FileTime.GuiApp.App.Services;
using FileTime.GuiApp.App.Settings;
using FileTime.GuiApp.App.ViewModels;
using FileTime.Providers.Local;
using Microsoft.Extensions.Configuration;
@@ -46,6 +48,7 @@ public static class Startup
serviceCollection.TryAddSingleton<GuiAppState>();
serviceCollection.TryAddSingleton<IAppState>(s => s.GetRequiredService<GuiAppState>());
serviceCollection.TryAddSingleton<IGuiAppState>(s => s.GetRequiredService<GuiAppState>());
serviceCollection.TryAddSingleton<ISettingsViewModel, SettingsViewModel>();
return serviceCollection;
}
@@ -67,6 +70,11 @@ public static class Startup
serviceCollection.TryAddSingleton<IUserCommunicationService>(s => s.GetRequiredService<IDialogService>());
serviceCollection.TryAddSingleton<IAppKeyService<Key>, GuiAppKeyService>();
serviceCollection.TryAddSingleton(new ApplicationConfiguration(false));
serviceCollection.TryAddSingleton<IDefaultBrowserRegister, DefaultBrowserRegister>();
serviceCollection.TryAddSingleton<IInstanceManager, InstanceManager>();
serviceCollection.TryAddSingleton<IInstanceMessageHandler, InstanceMessageHandler>();
serviceCollection.AddSingleton<IStartupHandler>(sp => sp.GetRequiredService<IInstanceManager>());
serviceCollection.AddSingleton<IExitHandler>(sp => sp.GetRequiredService<IInstanceManager>());
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{

View File

@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>