first commit
This commit is contained in:
63
Nitrox.Launcher/Models/Extensions/CloseByUserExtensions.cs
Normal file
63
Nitrox.Launcher/Models/Extensions/CloseByUserExtensions.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Nitrox.Launcher.Models.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Avalonia doesn't provide a public API to close the window non-programmatically so this is a hack to support it.
|
||||
/// </summary>
|
||||
public static class CloseByUserExtensions
|
||||
{
|
||||
private static readonly Dictionary<Window, bool> isClosingByUser = [];
|
||||
|
||||
/// <summary>
|
||||
/// Closes the window non-programmatically (by user).
|
||||
/// </summary>
|
||||
public static void CloseByUser(this Window window)
|
||||
{
|
||||
if (window == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
window.Closed += WindowOnClosed;
|
||||
isClosingByUser[window] = true;
|
||||
window.Close();
|
||||
|
||||
static void WindowOnClosed(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is not Window window)
|
||||
{
|
||||
return;
|
||||
}
|
||||
window.Closed -= WindowOnClosed;
|
||||
isClosingByUser.Remove(window);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the window programmatically.
|
||||
/// </summary>
|
||||
public static void CloseByCode(this Window window)
|
||||
{
|
||||
if (window == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
isClosingByUser[window] = false;
|
||||
window.Close();
|
||||
}
|
||||
|
||||
public static bool IsClosingByUser(this Window closingWindow, WindowClosingEventArgs closingArgs = null)
|
||||
{
|
||||
if (closingWindow is not null && isClosingByUser.TryGetValue(closingWindow, out bool isByUser))
|
||||
{
|
||||
return isByUser;
|
||||
}
|
||||
if (closingArgs is { IsProgrammatic: false })
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
15
Nitrox.Launcher/Models/Extensions/CollectionExtensions.cs
Normal file
15
Nitrox.Launcher/Models/Extensions/CollectionExtensions.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Nitrox.Launcher.Models.Extensions;
|
||||
|
||||
public static class CollectionExtensions
|
||||
{
|
||||
public static void AddRange<T>(this Collection<T> collection, params IEnumerable<T> items)
|
||||
{
|
||||
foreach (T item in items)
|
||||
{
|
||||
collection.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
53
Nitrox.Launcher/Models/Extensions/DialogServiceExtensions.cs
Normal file
53
Nitrox.Launcher/Models/Extensions/DialogServiceExtensions.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Threading;
|
||||
using HanumanInstitute.MvvmDialogs;
|
||||
using Nitrox.Launcher.Models.Utils;
|
||||
using Nitrox.Launcher.ViewModels;
|
||||
using Nitrox.Launcher.ViewModels.Abstract;
|
||||
using NitroxModel.Logger;
|
||||
|
||||
namespace Nitrox.Launcher.Models.Extensions;
|
||||
|
||||
public static class DialogServiceExtensions
|
||||
{
|
||||
public static async Task<T> ShowAsync<T, TExtra>(this IDialogService dialogService, Action<T, TExtra> setup = null, TExtra extraParameter = default) where T : ModalViewModelBase
|
||||
{
|
||||
try
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dialogService);
|
||||
// DataContext must be accessed on the UI thread, or it'll throw error.
|
||||
INotifyPropertyChanged owner = await Dispatcher.UIThread.InvokeAsync(() => AppViewLocator.MainWindow?.DataContext as INotifyPropertyChanged);
|
||||
if (owner == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Expected {nameof(AppViewLocator.MainWindow)}.{nameof(AppViewLocator.MainWindow.DataContext)} to not be null");
|
||||
}
|
||||
|
||||
T viewModel = dialogService.CreateViewModel<T>();
|
||||
setup?.Invoke(viewModel, extraParameter);
|
||||
bool? result = await dialogService.ShowDialogAsync<T>(owner, viewModel);
|
||||
if (result == true)
|
||||
{
|
||||
return viewModel;
|
||||
}
|
||||
return default;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"Failed to show dialog for ViewModel {typeof(T).FullName}");
|
||||
LauncherNotifier.Error(ex.Message);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<T> ShowAsync<T>(this IDialogService dialogService, Action<T> setup = null) where T : ModalViewModelBase => dialogService.ShowAsync<T, Action<T>>((model, act) => act?.Invoke(model), setup);
|
||||
|
||||
public static Task ShowErrorAsync(this IDialogService dialogService, Exception exception, string title = null, string description = null) =>
|
||||
dialogService.ShowAsync<DialogBoxViewModel>(model =>
|
||||
{
|
||||
model.Title = title ?? "Error";
|
||||
model.Description = string.IsNullOrWhiteSpace(description) ? exception.ToString() : $"{description}{Environment.NewLine}{exception}";
|
||||
model.ButtonOptions = ButtonOptions.OkClipboard;
|
||||
});
|
||||
}
|
17
Nitrox.Launcher/Models/Extensions/KeyValueStoreExtensions.cs
Normal file
17
Nitrox.Launcher/Models/Extensions/KeyValueStoreExtensions.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using NitroxModel.Helper;
|
||||
|
||||
namespace Nitrox.Launcher.Models.Extensions;
|
||||
|
||||
public static class KeyValueStoreExtensions
|
||||
{
|
||||
public static string GetSubnauticaLaunchArguments(this IKeyValueStore store, string defaultValue = "-vrmode none") => store == null ? defaultValue : store.GetValue("SubnauticaLaunchArguments", defaultValue);
|
||||
|
||||
public static void SetSubnauticaLaunchArguments(this IKeyValueStore store, string value)
|
||||
{
|
||||
if (store == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
store.SetValue("SubnauticaLaunchArguments", value);
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
namespace Nitrox.Launcher.Models.Extensions;
|
||||
|
||||
public static class MessageReceiverExtensions
|
||||
{
|
||||
public static void RegisterMessageListener<T, TReceiver>(this TReceiver receiver, Func<T, TReceiver, Task> asyncFunc) where T : class where TReceiver : IMessageReceiver
|
||||
{
|
||||
if (WeakReferenceMessenger.Default.IsRegistered<T>(receiver))
|
||||
{
|
||||
WeakReferenceMessenger.Default.Unregister<T>(receiver);
|
||||
}
|
||||
WeakReferenceMessenger.Default.Register<T>(receiver, (_, message) => asyncFunc(message, receiver));
|
||||
}
|
||||
|
||||
public static void RegisterMessageListener<T, TReceiver>(this TReceiver receiver, Action<T, TReceiver> action) where T : class where TReceiver : IMessageReceiver
|
||||
{
|
||||
if (WeakReferenceMessenger.Default.IsRegistered<T>(receiver))
|
||||
{
|
||||
WeakReferenceMessenger.Default.Unregister<T>(receiver);
|
||||
}
|
||||
WeakReferenceMessenger.Default.Register<T>(receiver, (_, message) => action(message, receiver));
|
||||
}
|
||||
}
|
33
Nitrox.Launcher/Models/Extensions/ProcessExExtensions.cs
Normal file
33
Nitrox.Launcher/Models/Extensions/ProcessExExtensions.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using NitroxModel.Platforms.OS.Shared;
|
||||
using NitroxModel.Platforms.OS.Windows;
|
||||
|
||||
namespace Nitrox.Launcher.Models.Extensions;
|
||||
|
||||
public static class ProcessExExtensions
|
||||
{
|
||||
public static void SetForegroundWindowAndRestore(this ProcessEx process)
|
||||
{
|
||||
if (Avalonia.Controls.Design.IsDesignMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
WindowsApi.BringProcessToFront(process.MainWindowHandle);
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
// xdotool sends an XEvent to X11 window manager on Linux systems.
|
||||
string command = $"xdotool windowactivate $(xdotool search --pid {process.Id} --onlyvisible --desktop '$(xdotool get_desktop)' --name 'nitrox launcher')";
|
||||
using Process proc = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "sh",
|
||||
ArgumentList = { "-c", command },
|
||||
});
|
||||
|
||||
// TODO: Support "bring to front" on Wayland window manager.
|
||||
}
|
||||
}
|
||||
}
|
88
Nitrox.Launcher/Models/Extensions/ScreenExtensions.cs
Normal file
88
Nitrox.Launcher/Models/Extensions/ScreenExtensions.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Nitrox.Launcher.Models.Design;
|
||||
using Nitrox.Launcher.Models.Utils;
|
||||
using Nitrox.Launcher.ViewModels.Abstract;
|
||||
|
||||
namespace Nitrox.Launcher.Models.Extensions;
|
||||
|
||||
public static class ScreenExtensions
|
||||
{
|
||||
private static readonly List<RoutableViewModelBase> navigationStack = [];
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to a view assigned to the given ViewModel.
|
||||
/// </summary>
|
||||
/// <param name="screen">The screen used to display the view.</param>
|
||||
/// <param name="routableViewModel">ViewModel that should be shown.</param>
|
||||
/// <typeparam name="TViewModel">Type of the ViewModel to show.</typeparam>
|
||||
public static async Task ShowAsync<TViewModel>(this IRoutingScreen screen, TViewModel routableViewModel) where TViewModel : RoutableViewModelBase
|
||||
{
|
||||
if (screen == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// When navigating away from a view in an async button command, busy states on buttons should also reset. Otherwise, when navigating back it would still show buttons being busy.
|
||||
NitroxAttached.AsyncCommandButtonTagger.Clear();
|
||||
if (screen.ActiveViewModel is RoutableViewModelBase routableViewModelBase)
|
||||
{
|
||||
navigationStack.RemoveAllFast(screen.ActiveViewModel, (item, param) => item.GetType() == param.GetType());
|
||||
await routableViewModelBase.ViewContentUnloadAsync();
|
||||
navigationStack.Add(routableViewModelBase);
|
||||
}
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
Task contentLoadTask = routableViewModel.ViewContentLoadAsync();
|
||||
if (screen.ActiveViewModel != null)
|
||||
{
|
||||
// Only show loading screen if page isn't loading super quickly.
|
||||
await Task.Delay(50);
|
||||
if (!contentLoadTask.IsCompleted)
|
||||
{
|
||||
screen.ActiveViewModel = AssetHelper.GetFullAssetPath("/Assets/Icons/loading.svg");
|
||||
await Task.Delay((int)Math.Max(0, 500 - sw.Elapsed.TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
await contentLoadTask;
|
||||
screen.ActiveViewModel = routableViewModel;
|
||||
}
|
||||
|
||||
public static async Task<bool> BackAsync(this IRoutingScreen screen)
|
||||
{
|
||||
if (navigationStack.Count < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
RoutableViewModelBase backViewModel = navigationStack[^1];
|
||||
navigationStack.Remove(backViewModel);
|
||||
await ShowAsync(screen, backViewModel);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to go back to the view assigned to the given ViewModel.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// True if ViewModel was found in the routing navigation stack. False when the ViewModel wasn't found and routing
|
||||
/// failed.
|
||||
/// </returns>
|
||||
public static async Task<bool> BackToAsync<T>(this IRoutingScreen screen) where T : RoutableViewModelBase
|
||||
{
|
||||
for (int i = navigationStack.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (navigationStack[i] is T target)
|
||||
{
|
||||
// Cleanup the stack up and including the back-target.
|
||||
for (int j = i; j < navigationStack.Count; j++)
|
||||
{
|
||||
navigationStack.RemoveAt(j);
|
||||
}
|
||||
await screen.ShowAsync(target);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
using HanumanInstitute.MvvmDialogs;
|
||||
using HanumanInstitute.MvvmDialogs.Avalonia;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Nitrox.Launcher.Models.Design;
|
||||
using Nitrox.Launcher.Models.Services;
|
||||
using Nitrox.Launcher.ViewModels.Abstract;
|
||||
using Nitrox.Launcher.Views.Abstract;
|
||||
using NitroxModel.Helper;
|
||||
using ServiceScan.SourceGenerator;
|
||||
|
||||
namespace Nitrox.Launcher.Models.Extensions;
|
||||
|
||||
public static partial class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddAppServices(this IServiceCollection collection)
|
||||
{
|
||||
// Avalonia and Reactive services
|
||||
collection.AddSingleton(provider => new AppViewLocator(provider));
|
||||
collection.AddSingleton<IRoutingScreen, RoutingScreen>();
|
||||
collection.AddSingleton<IDialogService>(provider => new DialogService(
|
||||
new DialogManager(
|
||||
provider.GetRequiredService<AppViewLocator>(),
|
||||
new DialogFactory()),
|
||||
provider.GetRequiredService));
|
||||
|
||||
// Domain services
|
||||
collection.AddSingleton(_ => KeyValueStore.Instance);
|
||||
collection.AddSingleton<ServerService>();
|
||||
|
||||
return collection
|
||||
.AddDialogs()
|
||||
.AddViews()
|
||||
.AddViewModels();
|
||||
}
|
||||
|
||||
[GenerateServiceRegistrations(AssignableTo = typeof(ModalViewModelBase), AsSelf = true)]
|
||||
[GenerateServiceRegistrations(AssignableTo = typeof(ModalBase), AsSelf = true)]
|
||||
private static partial IServiceCollection AddDialogs(this IServiceCollection services);
|
||||
|
||||
[GenerateServiceRegistrations(AssignableTo = typeof(RoutableViewBase<>), AsSelf = true, Lifetime = ServiceLifetime.Singleton)]
|
||||
private static partial IServiceCollection AddViews(this IServiceCollection services);
|
||||
|
||||
[GenerateServiceRegistrations(AssignableTo = typeof(ViewModelBase), AsSelf = true)]
|
||||
private static partial IServiceCollection AddViewModels(this IServiceCollection services);
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Platform.Storage;
|
||||
|
||||
namespace Nitrox.Launcher.Models.Extensions;
|
||||
|
||||
public static class StorageProviderExtensions
|
||||
{
|
||||
public static async Task<string> OpenFolderPickerAsync(this IStorageProvider storageProvider, string title, string startingFolder = null)
|
||||
{
|
||||
IStorageFolder startingStorageFolder = null;
|
||||
if (startingFolder != null)
|
||||
{
|
||||
startingStorageFolder = await storageProvider.TryGetFolderFromPathAsync(startingFolder);
|
||||
}
|
||||
IReadOnlyList<IStorageFolder> dialogResult = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = title,
|
||||
AllowMultiple = false,
|
||||
SuggestedStartLocation = startingStorageFolder
|
||||
});
|
||||
return dialogResult.FirstOrDefault()?.TryGetLocalPath() ?? "";
|
||||
}
|
||||
}
|
15
Nitrox.Launcher/Models/Extensions/StringExtensions.cs
Normal file
15
Nitrox.Launcher/Models/Extensions/StringExtensions.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.IO;
|
||||
|
||||
namespace Nitrox.Launcher.Models.Extensions;
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static string ReplaceInvalidFileNameCharacters(this string value)
|
||||
{
|
||||
foreach (char invalidFileNameChar in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
value = value.Replace(invalidFileNameChar, ' ');
|
||||
}
|
||||
return value.Trim();
|
||||
}
|
||||
}
|
34
Nitrox.Launcher/Models/Extensions/VisualExtensions.cs
Normal file
34
Nitrox.Launcher/Models/Extensions/VisualExtensions.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using NitroxModel.Platforms.OS.Windows;
|
||||
|
||||
namespace Nitrox.Launcher.Models.Extensions;
|
||||
|
||||
public static class VisualExtensions
|
||||
{
|
||||
public static void ApplyOsWindowStyling(this Visual visual)
|
||||
{
|
||||
if (Avalonia.Controls.Design.IsDesignMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (visual.GetWindow() is not { } window)
|
||||
{
|
||||
return;
|
||||
}
|
||||
nint? windowHandle = window.TryGetPlatformHandle()?.Handle;
|
||||
if (!windowHandle.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WindowsApi.EnableDefaultWindowAnimations(windowHandle.Value, window.CanResize);
|
||||
}
|
||||
|
||||
public static Window GetWindow(this Visual visual) => TopLevel.GetTopLevel(visual) as Window;
|
||||
}
|
Reference in New Issue
Block a user