Multi instance, tab handling
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
namespace FileTime.GuiApp.App.InstanceManagement.Messages;
|
||||
|
||||
[MessagePack.Union(0, typeof(OpenContainers))]
|
||||
public interface IInstanceMessage
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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("-", ",");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace FileTime.GuiApp.App.Settings;
|
||||
|
||||
public interface IDefaultBrowserRegister
|
||||
{
|
||||
void RegisterAsDefaultEditor();
|
||||
void UnregisterAsDefaultEditor();
|
||||
|
||||
void RegisterWinEShortcut();
|
||||
void UnregisterWinEShortcut();
|
||||
bool IsWinEShortcut();
|
||||
bool IsDefaultFileBrowser();
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace FileTime.GuiApp.App.Settings;
|
||||
|
||||
public enum SettingsPane
|
||||
{
|
||||
Home,
|
||||
Advanced
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user