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

@@ -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,7 +75,8 @@ 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,12 +147,13 @@ 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(
ex,
"An unhandled exception come from '{Caller}' exception handler from an object of type '{Type}' and value '{Value}': {Exception}",
caller,
sender?.GetType().ToString() ?? "null",
sender?.ToString() ?? "null",
ex);
=> _logger.Debug(
ex,
"An unhandled exception come from '{Caller}' exception handler from an object of type '{Type}' and value '{Value}': {Exception}",
caller,
sender?.GetType().ToString() ?? "null",
sender?.ToString() ?? "null",
ex);
}

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