Files
Daybreak/Daybreak.Core/Views/App.razor.cs

372 lines
13 KiB
C#

using Daybreak.Models;
using Daybreak.Services.Logging;
using Daybreak.Services.Notifications.Handlers;
using Daybreak.Shared.Models;
using Daybreak.Shared.Models.Menu;
using Daybreak.Shared.Services.Keyboard;
using Daybreak.Shared.Services.Menu;
using Daybreak.Shared.Services.Notifications;
using Daybreak.Shared.Services.Options;
using Daybreak.Shared.Services.Privilege;
using Daybreak.Shared.Services.Shell;
using Daybreak.Shared.Services.Themes;
using Daybreak.Shared.Services.Updater;
using Daybreak.Shared.Services.Window;
using Daybreak.Shared.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
using Photino.Blazor;
using System.Core.Extensions;
using System.Extensions;
using TrailBlazr.Services;
namespace Daybreak.Views;
public sealed class AppViewModel
{
private const string IssueUrl = "https://github.com/gwdevhub/Daybreak/issues/new";
private readonly IKeyboardHookService keyboardHookService;
private readonly IOptionsProvider optionsProvider;
private readonly IMenuServiceInitializer menuServiceInitializer;
private readonly IMenuServiceButtonHandler menuServiceButtonHandler;
private readonly IViewManager viewManager;
private readonly IThemeManager themeManager;
private readonly IApplicationUpdater applicationUpdater;
private readonly IPrivilegeManager privilegeManager;
private readonly PhotinoBlazorApp photinoApp;
private readonly JSConsoleInterop jsConsoleInterop;
private readonly INotificationService notificationService;
private readonly IWindowManipulationService windowManipulationService;
private readonly IShellExecutor shellExecutor;
private readonly ILogger<App> logger;
private SemaphoreSlim themeChangeSemaphore = new(1, 1);
private bool isInitialized = false;
public event EventHandler<WindowState>? WindowStateChanged;
public event EventHandler? RedrawRequested;
public Type CurrentViewType { get; private set; } = typeof(LaunchView);
public IDictionary<string, object> CurrentViewParameters { get; private set; } =
new Dictionary<string, object>();
public List<MenuCategory> MenuCategories { get; private set; } = [];
public WindowState WindowState { get; private set; }
public bool IsAdmin => this.privilegeManager.AdminPrivileges;
public bool IsNavigationOpen { get; private set; }
public string CurrentVersionText => this.applicationUpdater.CurrentVersion.ToString();
public INotificationProducer NotificationProducer { get; }
public string AccentBaseColor { get; set; } = string.Empty;
public string NeutralBaseColor { get; set; } = string.Empty;
public float BaseLayerLuminace { get; set; } = 0.0f;
public string? BackdropImage { get; set; } = string.Empty;
public string? BackdropImageFilter { get; set; } = string.Empty;
public string? BackdropEmbed { get; set; } = string.Empty;
public double UIScale { get; set; }
public double XXSmallFontSize { get; set; }
public double XSmallFontSize { get; set; }
public double SmallFontSize { get; set; }
public double MediumFontSize { get; set; }
public double LargeFontSize { get; set; }
public double XLargeFontSize { get; set; }
public double XXLargeFontSize { get; set; }
public AppViewModel(
IKeyboardHookService keyboardHookService,
IOptionsProvider optionsProvider,
IMenuServiceInitializer menuServiceInitializer,
IMenuServiceButtonHandler menuServiceButtonHandler,
IViewManager viewManager,
IThemeManager themeManager,
IApplicationUpdater applicationUpdater,
IPrivilegeManager privilegeManager,
PhotinoBlazorApp photinoApp,
JSConsoleInterop jsConsoleInterop,
INotificationProducer notificationProducer,
INotificationService notificationService,
IWindowManipulationService windowManipulationService,
IShellExecutor shellExecutor,
ILogger<App> logger)
{
this.keyboardHookService = keyboardHookService.ThrowIfNull();
this.optionsProvider = optionsProvider.ThrowIfNull();
this.menuServiceInitializer = menuServiceInitializer.ThrowIfNull();
this.menuServiceButtonHandler = menuServiceButtonHandler.ThrowIfNull();
this.viewManager = viewManager.ThrowIfNull();
this.themeManager = themeManager.ThrowIfNull();
this.applicationUpdater = applicationUpdater.ThrowIfNull();
this.privilegeManager = privilegeManager.ThrowIfNull();
this.photinoApp = photinoApp.ThrowIfNull();
this.jsConsoleInterop = jsConsoleInterop.ThrowIfNull();
this.NotificationProducer = notificationProducer.ThrowIfNull();
this.notificationService = notificationService.ThrowIfNull();
this.windowManipulationService = windowManipulationService.ThrowIfNull();
this.shellExecutor = shellExecutor.ThrowIfNull();
this.logger = logger.ThrowIfNull();
this.photinoApp.MainWindow.RegisterMaximizedHandler(this.OnWindowStateChanged)
.RegisterMinimizedHandler(this.OnWindowStateChanged)
.RegisterRestoredHandler(this.OnWindowStateChanged);
this.menuServiceInitializer.InitializeMenuService(
this.OpenNavigationMenu,
this.CloseNavigationMenu,
this.ToggleNavigationMenu
);
this.keyboardHookService.KeyUp += this.KeyboardHook_PreviewKeyUp;
}
private void KeyboardHook_PreviewKeyUp(object? _, KeyboardEventArgs eventArgs)
{
if (eventArgs.Key is VirtualKey.F1)
{
this.viewManager.ShowView<WikiView>((nameof(WikiView.Page), "Home"));
}
}
public async ValueTask InitializeApp(IJSRuntime jsRuntime)
{
if (this.isInitialized)
{
return;
}
this.themeManager.ThemeChanged += (s, e) => this.OnThemeChange();
this.OnThemeChange();
this.LoadMenuCategories();
await this.jsConsoleInterop.InitializeConsoleRedirection(jsRuntime);
this.RedrawRequested?.Invoke(this, EventArgs.Empty);
this.viewManager.ShowView<LaunchView>();
this.viewManager.ShowViewRequested += (s, e) => this.CloseNavigationMenu();
this.isInitialized = true;
}
public void Drag()
{
/*
* This operation has to be queued on the UI thread and has to be done via PostMessage to avoid re-entrancy.
* Using SendMessage or doing it directly can cause crashes when WebView2 is handling a user callback while another sendmessage appears.
*/
this.photinoApp.WindowManager.Dispatcher.InvokeAsync(() =>
{
this.windowManipulationService.DragWindow();
});
}
public void StartResize(ResizeDirection resizeDirection)
{
/*
* This operation has to be queued on the UI thread and has to be done via PostMessage to avoid re-entrancy.
* Using SendMessage or doing it directly can cause crashes when WebView2 is handling a user callback while another sendmessage appears.
*/
if (this.WindowState == WindowState.Maximized)
{
return;
}
this.photinoApp.WindowManager.Dispatcher.InvokeAsync(() =>
{
this.windowManipulationService.ResizeWindow(resizeDirection);
});
}
public void Minimize()
{
this.photinoApp.MainWindow.SetMinimized(true);
}
public void Maximize()
{
this.photinoApp.MainWindow.SetMaximized(true);
}
public void Restore()
{
this.photinoApp.MainWindow.SetMaximized(false);
}
public void Close()
{
this.photinoApp.MainWindow.Close();
}
public void ToggleNavigationMenu()
{
this.IsNavigationOpen = !this.IsNavigationOpen;
this.RedrawRequested?.Invoke(this, EventArgs.Empty);
}
public void OpenNavigationMenu()
{
this.IsNavigationOpen = true;
this.RedrawRequested?.Invoke(this, EventArgs.Empty);
}
public void CloseNavigationMenu()
{
this.IsNavigationOpen = false;
this.RedrawRequested?.Invoke(this, EventArgs.Empty);
}
public void OpenIssues()
{
this.shellExecutor.OpenUrl(IssueUrl);
}
public void OpenSynchronizationView()
{
this.viewManager.ShowView<SettingsSynchronizationView>();
}
public void OnError(ErrorEventArgs eventArgs)
{
if (eventArgs.GetException() is Exception ex)
{
this.notificationService.NotifyError<MessageBoxHandler>(
title: "Caught unhandled render exception",
description: ex.ToString()
);
}
else
{
this.notificationService.NotifyError<MessageBoxHandler>(
title: "Caught unhandled render exception",
description: "Could not retrieve exception"
);
}
}
public void MenuButtonClicked(MenuButton menuButton)
{
this.menuServiceButtonHandler.HandleButton(menuButton);
}
public void RestartAsNormalUser()
{
this.privilegeManager.RequestNormalPrivileges<LaunchView>(
"You are currently running Daybreak as administrator",
default,
CancellationToken.None
);
}
private async void OnThemeChange()
{
using var ctx = await this.themeChangeSemaphore.Acquire();
this.BackdropImage = !string.IsNullOrWhiteSpace(this.themeManager.BackdropImage) ? this.GetBackdropImageUrl(this.themeManager.BackdropImage) : default;
this.BackdropEmbed = !string.IsNullOrWhiteSpace(this.themeManager.BackdropEmbed) ? this.themeManager.BackdropEmbed : default;
this.BackdropImageFilter = this.themeManager.CurrentTheme?.Filter ?? string.Empty;
this.BaseLayerLuminace = this.themeManager.BaseLayerLuminance;
this.AccentBaseColor = this.themeManager.AccentBaseColorHex;
this.NeutralBaseColor = this.themeManager.NeutralBaseColorHex;
this.XXSmallFontSize = this.themeManager.XXSmallFontSize;
this.XSmallFontSize = this.themeManager.XSmallFontSize;
this.SmallFontSize = this.themeManager.SmallFontSize;
this.MediumFontSize = this.themeManager.MediumFontSize;
this.LargeFontSize = this.themeManager.LargeFontSize;
this.XLargeFontSize = this.themeManager.XLargeFontSize;
this.XXLargeFontSize = this.themeManager.XXLargeFontSize;
this.UIScale = this.themeManager.UIScale;
this.RedrawRequested?.Invoke(this, EventArgs.Empty);
}
private void LoadMenuCategories()
{
foreach (var category in this.menuServiceInitializer.GetCategories())
{
this.MenuCategories.Add(category);
if (category.Name is "Settings")
{
foreach (var option in this.optionsProvider.GetRegisteredOptionDefinitions())
{
category.RegisterButton(
option.Name,
option.Description,
sp =>
sp.GetRequiredService<IViewManager>()
.ShowView<OptionView>(("optionName", option.Name))
);
}
}
}
}
private string GetBackdropImageUrl(string backdropPath)
{
if (string.IsNullOrWhiteSpace(backdropPath))
{
return string.Empty;
}
// Relative paths are served from wwwroot
if (!Path.IsPathRooted(backdropPath))
{
var normalizedPath = backdropPath.Replace('\\', '/');
if (!normalizedPath.StartsWith('/'))
{
normalizedPath = "/" + normalizedPath;
}
return $"url('{normalizedPath}')";
}
// Absolute paths need to be embedded as base64
return this.ConvertImageToBase64DataUri(backdropPath);
}
private string ConvertImageToBase64DataUri(string filePath)
{
try
{
if (!File.Exists(filePath))
{
this.logger.LogWarning("Backdrop file does not exist: {FilePath}", filePath);
return string.Empty;
}
var extension = Path.GetExtension(filePath).ToLowerInvariant();
var mimeType = extension switch
{
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".bmp" => "image/bmp",
".webp" => "image/webp",
_ => "image/jpeg"
};
var imageBytes = File.ReadAllBytes(filePath);
var base64 = System.Convert.ToBase64String(imageBytes);
return $"url('data:{mimeType};base64,{base64}')";
}
catch (Exception ex)
{
this.logger.LogError(ex, "Failed to convert image to base64: {FilePath}", filePath);
return string.Empty;
}
}
private void OnWindowStateChanged(object? _, EventArgs __)
{
this.WindowState = this.photinoApp.MainWindow.Maximized ?
WindowState.Maximized :
this.photinoApp.MainWindow.Minimized ?
WindowState.Minimized :
WindowState.Normal;
this.photinoApp.WindowManager.Dispatcher.InvokeAsync(() =>
{
this.WindowStateChanged?.Invoke(this, this.WindowState);
});
}
}