fix issues with new UI, add color profiles, add sdr brightness for AMD

This commit is contained in:
Vincent Maas committed 2024-09-01 16:24:05 +02:00
1 parent 4d2ca8a552
commit f42a46e53f
95 files changed
+12129 -10005

No files matched your search

+4 -1
View File
@@ -1,5 +1,5 @@
using ColorControl.Shared.Contracts;
using ColorControl.Shared.Services;
using ColorControl.UI.Services;
namespace ColorControl.UI;
@@ -15,6 +15,9 @@ public static class Blazor
builder.Services.AddSingleton<AppState>(new AppState { SelectedTheme = (config?.UseDarkMode ?? true) ? "dark" : "light" });
builder.Services.AddTransient<RpcUiClientService>();
builder.Services.AddTransient<JSHelper>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<NotificationService>();
builder.WebHost.ConfigureKestrel(o => o.ListenAnyIP(config?.UiPort > 0 ? config.UiPort : 5000));
var app = builder.Build();
+16
View File
@@ -13,9 +13,13 @@
<ItemGroup>
<ProjectReference Include="..\Shared\Shared.csproj" />
<ProjectReference Include="..\MHC2Gen\MHC2Gen.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="Components\Pages\ColorProfile\ColorProfilePage.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Generic\FieldDefs.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
@@ -25,6 +29,9 @@
<Content Update="Components\Pages\Game\GameSummary.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Generic\NotificationsComponent.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Generic\TriggerInput.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
@@ -52,6 +59,15 @@
<Content Update="Components\Pages\Samsung\SamsungServiceMenu.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="wwwroot\app.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\favicon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\helper.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
+19 -7
View File
@@ -1,8 +1,15 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services
@using ColorControl.Shared.Common
@using ColorControl.Shared.Contracts
@using ColorControl.UI.Services
@using ColorControl.UI.Components.Pages.Generic
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject AppState AppState
@inject RpcUiClientService ClientService;
@inject NavigationManager _navigationManager;
@inject RpcUiClientService _rpcClientService
@inject NavigationManager _navigationManager
@inject NotificationService _notificationService
@inject ProtectedSessionStorage _sessionStorage
@implements IDisposable
@attribute [StreamRendering]
@@ -15,28 +22,33 @@
<base href="/" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="ColorControl.UI.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
<NotificationsComponent></NotificationsComponent>
</body>
</html>
@code {
private Config? Config;
protected override async Task OnInitializedAsync()
{
var config = await ClientService.CallAsync<Config>("OptionsService", "GetConfig");
Config ??= await _rpcClientService.CallAsync<Config>("OptionsService", "GetConfig");
var newTheme = config.UseDarkMode ? "dark" : "light";
var newTheme = Config?.UseDarkMode == true ? "dark" : "light";
if (newTheme != AppState.SelectedTheme)
{
AppState.SelectedTheme = config.UseDarkMode ? "dark" : "light";
AppState.SelectedTheme = Config?.UseDarkMode == true ? "dark" : "light";
_navigationManager.Refresh();
}
@@ -1,5 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.UI.Services
@rendermode InteractiveServer
@@ -28,28 +29,34 @@
@foreach (var module in modules.Where(m => m.IsActive))
{
<div class="nav-item px-3">
<NavLink class="nav-link px-3" href="@module.Link">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> @module.DisplayName
<NavLink class="nav-link px-3 align-items-baseline" href="@module.Link">
<span class="bi bi-@module.GetIconClass()" aria-hidden="true"></span> @module.DisplayName
</NavLink>
</div>
}
}
<div class="nav-item px-3">
<NavLink class="nav-link px-3" href="options">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Options
<NavLink class="nav-link px-3 align-items-baseline" href="options">
<span class="bi bi-gear-fill" aria-hidden="true"></span> Options
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link px-3" href="logging">
<span class="bi bi-list-logging-nav-menu" aria-hidden="true"></span> Logging
<NavLink class="nav-link px-3 align-items-baseline" href="ColorProfile">
<span class="bi bi-palette-fill" aria-hidden="true"></span> Color Profiles
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link px-3" href="info">
<span class="bi bi-list-logging-nav-menu" aria-hidden="true"></span> Info
<NavLink class="nav-link px-3 align-items-baseline" href="logging">
<span class="bi bi-list-columns-reverse" aria-hidden="true"></span> Logging
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link px-3 align-items-baseline" href="info">
<span class="bi bi-info-circle-fill" aria-hidden="true"></span> Info
</NavLink>
</div>
</nav>
@@ -1,5 +1,5 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services
@using ColorControl.UI.Services
@inherits LayoutComponentBase
@inject AppState AppState
@@ -1,15 +1,15 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.AMD
@using ColorControl.Shared.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.Shared.Common;
@using ColorControl.UI.Services
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
<div class="modal" id="presetModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" @onfocus="ModalOnFocus">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(Preset?.IsDisplayPreset == true ? $"Settings for display {Preset?.displayName}" : Preset?.id == 0 ? $"Settings for new preset" : $"Settings of preset {Preset?.IdOrName}")</h5>
@@ -316,7 +316,7 @@
selectedVirtualResolution = Preset?.DisplayConfig.Resolution?.GetVirtualString();
selectedRefreshRate = Preset?.DisplayConfig.RefreshRate?.ToString();
if (Preset?.IsDisplayPreset != true)
if (Preset?.IsDisplayPreset == false)
{
displayPresets = await _rpcClientService.CallAsync<List<AmdPreset>>("AmdService", "GetDisplayPresets");
}
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.AMD
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@@ -8,7 +8,7 @@
@inject RpcUiClientService _rpcClientService
<div class="modal" id="settingsModal" tabindex="-1" @onfocus="OnFocus">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">AMD settings</h5>
@@ -3,7 +3,7 @@
@using ColorControl.Shared.Common
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.AMD
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@@ -92,6 +92,10 @@ else
</div>
<div class="row mb-3">
<div class="col">
@if (!Presets.Any())
{
<span>No presets found</span>
}
<div class="list-group">
@foreach (var preset in Presets)
{
@@ -106,11 +110,21 @@ else
}
</h5>
</div>
<span>
<span class="d-none d-lg-block">
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Delete</button>
<button class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Clone</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Edit</button>
</span>
<div class="d-lg-none" @onclick:preventDefault @onclick:stopPropagation>
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)">Delete</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)">Clone</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)">Edit</a></li>
</ul>
</div>
</div>
<p class="mb-1">@preset.DisplayConfig.Resolution.ToString() @@ @preset.DisplayConfig.RefreshRate.ToString()Hz @(preset.applyColorData ? preset.colorDepth : "")</p>
<small class="text-muted">@preset.shortcut</small>
@@ -124,7 +138,6 @@ else
@code {
private List<AmdPreset>? Presets;
private List<AmdPreset>? DisplayPresets;
private List<VirtualResolution>? Resolutions;
private AmdPreset? CurrentPreset;
@@ -134,7 +147,6 @@ else
{
await RefreshPresets();
DisplayPresets = await _rpcClientService.CallAsync<List<AmdPreset>>("AmdService", "GetDisplayPresets");
Resolutions = await _rpcClientService.CallAsync<List<VirtualResolution>>("AmdService", "GetAvailableResolutionsV2", DisplayPresets?.FirstOrDefault());
}
protected override async Task OnAfterRenderAsync(bool firstTime)
@@ -0,0 +1,452 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.AMD
@using ColorControl.Shared.Contracts.DisplayInfo
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.Shared.Common;
@using ColorControl.UI.Services
@using MHC2Gen
@using Microsoft.JSInterop;
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject JSHelper jsHelper;
<div class="modal modal-lg" id="colorProfileModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" @onfocus="ModalOnFocus">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(ColorProfile?.Name == null ? $"Settings for new color profile" : $"Settings of color profile {ColorProfile.Name}")</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="() => IsVisible = false"></button>
</div>
<div class="modal-body">
@if (ColorProfile?.HasExtraInfo == false)
{
<div class="alert alert-warning" role="alert">
This is not a profile created by ColorControl. Some fields are set to default.
</div>
}
@if (!string.IsNullOrEmpty(_rpcClientService.LastErrorMessage))
{
<div class="alert alert-danger" role="alert">
Error: @_rpcClientService.LastErrorMessage
</div>
}
<form class="row g-1 needs-validation @WasValidated" id="profileForm" novalidate>
@if (ColorProfile == null || DisplayInfo == null || CurrentColorInfo == null)
{
<div>Loading...</div>
}
else
{
<div class="mb-2">
<label class="form-label" for="display">Display</label>
<input class="form-control" type="text" id="display" value="@DisplayInfo" disabled />
</div>
<div class="mb-2">
<label class="form-label" for="name">Name</label>
<input class="form-control" type="text" id="name" @bind="ColorProfile.Name" @bind:after="AfterNameChanged" required />
</div>
<div class="mb-2">
<label class="form-label" for="description">Description</label>
<input class="form-control" type="text" id="description" @bind="ColorProfile.Description" />
</div>
<div class="accordion accordion-flush" id="presetAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<span class="d-flex align-items-baseline">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#displaySettings">
Color gamut
</button>
</span>
</h2>
<div id="displaySettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<div class="row">
<div class="col">
<label class="form-label" for="primariesSource"><small>Show Display Primaries and Luminance values of</small></label>
<select class="form-select" id="primariesSource" @bind="DisplayPrimariesSource" @bind:after="DisplayPrimariesSourceChanged">
@foreach (var colorInfo in DisplayColors)
{
<option value="@colorInfo.DisplayPrimariesSource">@(colorInfo.CustomName ?? colorInfo.DisplayPrimariesSource.ToString())</option>
}
</select>
</div>
<div class="col align-content-end">
<button class="btn btn-primary" type="button" disabled="@(DisplayPrimariesSource == DisplayPrimariesSource.ColorProfile)" @onclick="OverwritePrimariesOnClick">Overwrite color profile values</button>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label text-danger" for="redX">Red X-coordinate</label>
<input class="form-control" id="redX" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.Red.X" disabled="@PrimariesDisabled" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label text-danger" for="redY">Red Y-coordinate</label>
<input class="form-control" id="redY" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.Red.Y" disabled="@PrimariesDisabled" />
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label text-success" for="greenX">Green X-coordinate</label>
<input class="form-control" id="greenX" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.Green.X" disabled="@PrimariesDisabled" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label text-success" for="greenY">Green Y-coordinate</label>
<input class="form-control" id="greenY" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.Green.Y" disabled="@PrimariesDisabled" />
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label text-primary" for="blueX">Blue X-coordinate</label>
<input class="form-control" id="blueX" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.Blue.X" disabled="@PrimariesDisabled" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label text-primary" for="blueY">Blue Y-coordinate</label>
<input class="form-control" id="blueY" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.Blue.Y" disabled="@PrimariesDisabled" />
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="whiteX">White X-coordinate</label>
<input class="form-control" id="whiteX" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.White.X" disabled="@PrimariesDisabled" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="whiteY">White Y-coordinate</label>
<input class="form-control" id="whiteY" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.White.Y" disabled="@PrimariesDisabled" />
</div>
</div>
</div>
<div class="mb-2">
<label class="form-label" for="targetGamut">Target gamut</label>
<select class="form-select" id="targetGamut" @bind="ColorProfile.ColorGamut">
@foreach (var gamut in Enum.GetValues<ColorGamut>())
{
<option value="@gamut">@gamut.GetDescription()</option>
}
</select>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<span class="d-flex align-items-baseline">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#luminanceSettings">
Luminance settings
</button>
</span>
</h2>
<div id="luminanceSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="minLuminance">Minimum luminance (nits)</label>
<input class="form-control" id="minLuminance" type="number" min="0" max="10" @bind="CurrentColorInfo.BlackLuminance" disabled="@PrimariesDisabled" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="maxLuminance">Max full frame luminance (nits)</label>
<input class="form-control" id="maxLuminance" type="number" min="0" max="10000" @bind="CurrentColorInfo.WhiteLuminance" disabled="@PrimariesDisabled" />
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="minCLL">Minimum brightness (MinCLL) (nits)</label>
<input class="form-control" id="minCLL" type="number" min="0" max="10" @bind="CurrentColorInfo.MinCLL" disabled="@PrimariesDisabled" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="maxCLL">Peak brightness (MaxCLL) (nits)</label>
<input class="form-control" id="maxCLL" type="number" min="0" max="10000" @bind="CurrentColorInfo.MaxCLL" disabled="@PrimariesDisabled" />
</div>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<span class="d-flex align-items-baseline">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#hdrSettings">
Advanced EOTF settings
</button>
</span>
</h2>
<div id="hdrSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="sdrTransfer">SDR Transfer Function</label>
<select class="form-select" id="sdrTransfer" @bind="ColorProfile.SDRTransferFunction">
@foreach (var function in Enum.GetValues<SDRTransferFunction>())
{
<option value="@function">@function.GetDescription()</option>
}
</select>
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="gamma">Gamma</label>
<input class="form-control" id="gamma" type="number" min="0.1" max="10" step="0.01" @bind="ColorProfile.Gamma" disabled="@(ColorProfile.SDRTransferFunction != SDRTransferFunction.PurePower)" />
</div>
</div>
</div>
@if (ColorProfile.IsHDRProfile)
{
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="minSDR">Minimum SDR brightness (nits)</label>
<input class="form-control" id="minSDR" type="number" min="0" max="10" step="0.001" @bind="ColorProfile.SDRMinBrightness" disabled="@(ColorProfile.SDRTransferFunction != SDRTransferFunction.PurePower)" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="maxSDR">Maximum SDR brightness (nits)</label>
<input class="form-control" id="maxSDR" type="number" min="0" max="10000" @bind="ColorProfile.SDRMaxBrightness" disabled="@(ColorProfile.SDRTransferFunction != SDRTransferFunction.PurePower)" />
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="brightnessBoost">Brightness boost (%)</label>
<input class="form-control" id="brightnessBoost" type="number" min="-100" max="100" @bind="ColorProfile.SDRBrightnessBoost" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="shadowDetailBoost">Shadow detail boost (%)</label>
<input class="form-control" id="shadowDetailBoost" type="number" min="0" max="100" @bind="ColorProfile.ShadowDetailBoost" disabled="@(ColorProfile.SDRTransferFunction == SDRTransferFunction.Piecewise)" />
</div>
</div>
</div>
}
</div>
</div>
</div>
</div>
}
</form>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary" @onclick="InstallOnClick" disabled="@(ColorProfile == null)">Install and set as default</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="() => IsVisible = false">Close</button>
<button type="submit" class="btn btn-primary" @onclick="ApplyClick" disabled="@(ColorProfile == null)">Save</button>
</div>
</div>
</div>
</div>
@code {
private ColorProfileDto? ColorProfile { get; set; }
private DisplayColorInfo? CurrentColorInfo { get; set; }
private DisplayColorInfo? ColorProfileColorInfo { get; set; }
private List<DisplayColorInfo> DisplayColors { get; set; } = new();
private bool PrimariesDisabled = false;
[Parameter]
public string? ColorProfileName { get; set; }
[Parameter]
public bool UseAsTemplate { get; set; }
[Parameter]
public DisplayInfoDto? DisplayInfo { get; set; }
[Parameter]
public Func<ColorProfileDto, Task>? AfterApply { get; set; }
private string WasValidated = "";
private bool IsVisible;
private DisplayPrimariesSource DisplayPrimariesSource;
private bool IsInstalled;
protected override async Task OnParametersSetAsync()
{
if (ColorProfileName == null)
{
return;
}
if (ColorProfileName == "")
{
ColorProfile = ColorProfileDto.CreateDefault(DisplayInfo?.IsHdrSupportedAndEnabled ?? false);
}
else
{
ColorProfile = await _rpcClientService.CallAsync<ColorProfileDto>("ColorProfileService", "GetColorProfile", ColorProfileName);
}
if (ColorProfile != null)
{
ColorProfileColorInfo = new DisplayColorInfo
{
DisplayPrimariesSource = DisplayPrimariesSource.ColorProfile,
BlackLuminance = ColorProfile.BlackLuminance,
WhiteLuminance = ColorProfile.WhiteLuminance,
MinCLL = ColorProfile.MinCLL,
MaxCLL = ColorProfile.MaxCLL,
RgbPrimaries = new RgbPrimariesDto(ColorProfile.DevicePrimaries)
};
CurrentColorInfo = ColorProfileColorInfo;
DisplayColors.Clear();
DisplayColors.Add(CurrentColorInfo);
if (DisplayInfo != null)
{
DisplayColors.AddRange(DisplayInfo.DisplayColors);
}
if (UseAsTemplate)
{
if (ColorProfile.Name != null)
{
ColorProfile.Name = Path.GetFileNameWithoutExtension(ColorProfile.Name) + " (clone).icm";
}
else
{
ColorProfile.Name = "";
}
}
}
}
private async Task ApplyClick(MouseEventArgs e)
{
if (ColorProfile == null || CurrentColorInfo == null)
{
return;
}
if (!(await IsValid()))
{
return;
}
ColorProfile.UpdatePrimariesAndLuminance(CurrentColorInfo);
var result = await _rpcClientService.CallAsync<bool>("ColorProfileService", "UpdateColorProfile", ColorProfile, DisplayInfo?.DeviceName);
if (result)
{
await jsHelper.CloseModal("colorProfileModal");
IsVisible = false;
if (AfterApply != null)
{
await AfterApply.Invoke(ColorProfile);
}
}
}
private async Task InstallOnClick()
{
if (ColorProfile == null || DisplayInfo == null)
{
return;
}
if (!(await IsValid()))
{
return;
}
ColorProfile.UpdatePrimariesAndLuminance(CurrentColorInfo);
var result = await _rpcClientService.CallAsync<bool>("ColorProfileService", "InstallAndActivateColorProfile", ColorProfile, DisplayInfo.DeviceName);
IsInstalled = true;
}
private void ModalOnFocus(FocusEventArgs e)
{
if (!IsVisible)
{
WasValidated = "";
IsVisible = true;
DisplayPrimariesSource = DisplayPrimariesSource.ColorProfile;
}
}
private async Task<bool> IsValid()
{
var valid = await jsHelper.IsFormValid("profileForm");
if (!valid)
{
WasValidated = "was-validated";
}
return valid;
}
private void DisplayPrimariesSourceChanged()
{
if (DisplayInfo == null || ColorProfile == null)
{
return;
}
CurrentColorInfo = DisplayColors.First(dc => dc.DisplayPrimariesSource == DisplayPrimariesSource);
PrimariesDisabled = CurrentColorInfo.DisplayPrimariesSource != DisplayPrimariesSource.ColorProfile;
}
private void OverwritePrimariesOnClick(MouseEventArgs e)
{
if (ColorProfileColorInfo == null || CurrentColorInfo == null)
{
return;
}
ColorProfileColorInfo.BlackLuminance = CurrentColorInfo.BlackLuminance;
ColorProfileColorInfo.WhiteLuminance = CurrentColorInfo.WhiteLuminance;
ColorProfileColorInfo.MinCLL = CurrentColorInfo.MinCLL;
ColorProfileColorInfo.MaxCLL = CurrentColorInfo.MaxCLL;
ColorProfileColorInfo.RgbPrimaries = new RgbPrimariesDto(CurrentColorInfo.RgbPrimaries);
}
private void AfterNameChanged()
{
if (ColorProfile?.Name == null)
{
return;
}
if (!ColorProfile.Name.EndsWith(".icm", StringComparison.InvariantCultureIgnoreCase))
{
ColorProfile.Name += ".icm";
}
}
}
@@ -0,0 +1,301 @@
@page "/ColorProfile"
@using ColorControl.Shared.Common
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.DisplayInfo
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Generics
@using ColorControl.UI.Services
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject JSHelper jsHelper;
<PageTitle>Color profiles</PageTitle>
<ColorProfilePage ColorProfileName="@SelectedColorProfileName" UseAsTemplate="UseAsTemplate" DisplayInfo="SelectedDisplay" AfterApply="AfterColorProfileSaved"></ColorProfilePage>
<ConfirmModal Title="Delete color profile" Message="Are you sure you want to delete the color profile?" OnConfirm="ColorProfileOnDeleteConfirmed" />
<ConfirmModal ModalId="disassociateConfirmModal" Title="Disassociate color profile" Message="Are you sure you want to disassociate the color profile from the selected display?" OnConfirm="ColorProfileOnDisassociateConfirmed" />
<ConfirmModal ModalId="associateConfirmModal" Title="Associate color profile" Message="Are you sure you want to associate the color profile with the selected display?" OnConfirm="ColorProfileOnAssociateConfirmed" />
<h2>Displays</h2>
@if (Displays == null)
{
<div>Loading...</div>
}
else
{
<div class="row mb-3">
@if (!Displays.Any())
{
<span>No displays found</span>
}
@foreach (var display in Displays)
{
<div class="mb-2">
<label class="form-label" for="device">Selected display</label>
<select class="form-select" id="device" @bind="SelectedDisplayId" @bind:after="SelectedDisplayChanged">
@foreach (var device in Displays)
{
<option value="@display.DisplayId">@display.DisplayName @@ @display.AdapterName</option>
}
</select>
</div>
@* <div class="col-lg-6 mb-3">
<div class="card h-100">
<div class="card-header d-flex align-items-baseline">
<span>
@display.DisplayName @@ @display.AdapterName
</span>
</div>
<div class="card-body">
<small class="text-body">@display.DevicePath</small>
<small class="text-muted">EDID? @display.DisplayColors.Any(dc => dc.DisplayPrimariesSource == MHC2Gen.DisplayPrimariesSource.EDID)</small>
</div>
</div>
</div>
*@ }
</div>
}
@if (SelectedDisplay == null)
{
<div>Loading...</div>
}
else
{
<div class="mb-2 d-flex align-items-center">
<h2>Display Associated Color profiles (@(SelectedDisplay.IsHdrSupportedAndEnabled ? "HDR" : "SDR"))</h2>
<div class="d-flex ms-auto">
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => AddColorProfileOnClick()">
Add
</button>
</div>
</div>
<div class="row mb-3">
<div class="col">
@if (!SelectedDisplay.ColorProfiles.Any())
{
<span>No color profiles found that are associated with the display</span>
}
else
{
<div class="list-group">
@foreach (var colorProfile in SelectedDisplay.ColorProfiles)
{
<a class="list-group-item list-group-item-action" @onclick="() => SetColorProfileOnClick(colorProfile)" @onclick:preventDefault @onclick:stopPropagation>
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="mb-1">
@colorProfile
@if (colorProfile == SelectedDisplay?.DefaultColorProfile)
{
<span class="ms-2 badge text-bg-warning" title="Active color profile">Active</span>
}
</h5>
</div>
<span class="d-none d-lg-block">
<button class="btn btn-warning btn-sm" data-bs-toggle="modal" data-bs-target="#disassociateConfirmModal" @onclick="() => SelectedColorProfileName = colorProfile" @onclick:preventDefault @onclick:stopPropagation>Disassociate</button>
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => DeleteColorProfileOnClick(colorProfile)" @onclick:preventDefault @onclick:stopPropagation>Delete</button>
<button class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => CloneColorProfileOnClick(colorProfile)" @onclick:preventDefault @onclick:stopPropagation>Clone</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => EditColorProfileOnClick(colorProfile)" @onclick:preventDefault @onclick:stopPropagation>Edit</button>
</span>
<div class="d-lg-none" @onclick:preventDefault @onclick:stopPropagation>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#disassociateConfirmModal" @onclick="() => SelectedColorProfileName = colorProfile">Disassociate</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => DeleteColorProfileOnClick(colorProfile)">Delete</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => CloneColorProfileOnClick(colorProfile)">Clone</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => EditColorProfileOnClick(colorProfile)">Edit</a></li>
</ul>
</div>
</div>
</a>
}
</div>
}
</div>
</div>
}
<div class="mb-2 d-flex align-items-center">
<h2>Other Color profiles</h2>
</div>
<div class="row mb-3">
<div class="col">
@if (ColorProfileNames == null)
{
<span>No profiles found</span>
}
else
{
<div class="list-group">
@foreach (var colorProfile in ColorProfileNames)
{
<span class="list-group-item list-group-item-action" @onclick="() => EditColorProfileOnClick(colorProfile)" @onclick:preventDefault @onclick:stopPropagation>
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="mb-1">
@colorProfile
</h5>
</div>
<span class="d-none d-lg-block">
<button class="btn btn-warning btn-sm" data-bs-toggle="modal" data-bs-target="#associateConfirmModal" @onclick="() => SelectedColorProfileName = colorProfile" @onclick:preventDefault @onclick:stopPropagation>Associate</button>
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => DeleteColorProfileOnClick(colorProfile)" @onclick:preventDefault @onclick:stopPropagation>Delete</button>
<button class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => CloneColorProfileOnClick(colorProfile)" @onclick:preventDefault @onclick:stopPropagation>Clone</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => EditColorProfileOnClick(colorProfile)" @onclick:preventDefault @onclick:stopPropagation>Edit</button>
</span>
<div class="d-lg-none" @onclick:preventDefault @onclick:stopPropagation>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#associateConfirmModal" @onclick="() => SelectedColorProfileName = colorProfile">Associate</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => DeleteColorProfileOnClick(colorProfile)">Delete</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => CloneColorProfileOnClick(colorProfile)">Clone</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => EditColorProfileOnClick(colorProfile)">Edit</a></li>
</ul>
</div>
</div>
</span>
}
</div>
}
</div>
</div>
@code {
private string? SelectedDisplayId;
private DisplayInfoDto? SelectedDisplay;
private List<DisplayInfoDto>? Displays;
private string? SelectedColorProfileName;
private bool UseAsTemplate;
private List<string>? ColorProfileNames;
protected override async Task OnInitializedAsync()
{
await RefreshDisplays();
}
protected override async Task OnAfterRenderAsync(bool firstTime)
{
}
private async Task AddColorProfileOnClick()
{
SelectedColorProfileName = "";
}
private async Task EditColorProfileOnClick(string colorProfile)
{
UseAsTemplate = false;
SelectedColorProfileName = colorProfile;
await jsHelper.OpenModal("colorProfileModal");
}
private async Task SetColorProfileOnClick(string colorProfile)
{
if (SelectedDisplay == null)
{
return;
}
var result = await _rpcClientService.CallAsync<bool>("ColorProfileService", "SetDisplayDefaultColorProfile", SelectedDisplay.DeviceName, colorProfile);
if (result)
{
SelectedDisplay.DefaultColorProfile = colorProfile;
}
}
private async Task DeleteColorProfileOnClick(string colorProfile)
{
SelectedColorProfileName = colorProfile;
}
private async Task CloneColorProfileOnClick(string colorProfile)
{
UseAsTemplate = true;
SelectedColorProfileName = colorProfile;
}
private async Task ColorProfileOnDeleteConfirmed()
{
if (SelectedColorProfileName == null)
{
return;
}
await _rpcClientService.CallAsync<bool>("ColorProfileService", "DeleteColorProfile", SelectedColorProfileName);
SelectedColorProfileName = null;
await RefreshDisplays();
StateHasChanged();
}
private async Task ColorProfileOnDisassociateConfirmed()
{
if (SelectedColorProfileName == null || SelectedDisplay == null)
{
return;
}
await _rpcClientService.CallAsync<bool>("ColorProfileService", "RemoveDisplayColorProfile", SelectedDisplay.DeviceName, SelectedColorProfileName);
SelectedColorProfileName = null;
await RefreshDisplays();
StateHasChanged();
}
private async Task ColorProfileOnAssociateConfirmed()
{
if (SelectedColorProfileName == null || SelectedDisplay == null)
{
return;
}
await _rpcClientService.CallAsync<bool>("ColorProfileService", "AddDisplayColorProfile", SelectedDisplay.DeviceName, SelectedColorProfileName, false);
SelectedColorProfileName = null;
await RefreshDisplays();
StateHasChanged();
}
private void SelectedDisplayChanged()
{
SelectedDisplay = Displays?.FirstOrDefault(d => d.DisplayId == SelectedDisplayId);
}
private async Task AfterColorProfileSaved(ColorProfileDto colorProfile)
{
await RefreshDisplays();
StateHasChanged();
}
private async Task RefreshDisplays()
{
Displays = await _rpcClientService.CallAsync<List<DisplayInfoDto>>("ColorProfileService", "GetDisplayInfos");
ColorProfileNames = await _rpcClientService.CallAsync<List<string>>("ColorProfileService", "GetAllColorProfileNames");
if (Displays?.Any() == true)
{
SelectedDisplay = Displays.FirstOrDefault(d => d.DisplayId == SelectedDisplayId) ?? Displays.First();
SelectedDisplayId = SelectedDisplay.DisplayId;
ColorProfileNames = ColorProfileNames?.Except(SelectedDisplay.ColorProfiles).ToList();
}
}
}
@@ -1,7 +1,7 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.Game
@using ColorControl.Shared.Services
@using ColorControl.UI.Services
@using ColorControl.UI.Components.Pages.Generic
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common
@@ -13,7 +13,7 @@
@inject NavigationManager _navigationManager;
<div class="modal" id="presetModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" @onfocus="ModalOnFocus">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(Preset?.id == 0 ? $"Settings for new preset" : $"Settings of preset {Preset?.IdOrName}")</h5>
@@ -1,7 +1,7 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.Game
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@@ -9,7 +9,7 @@
@inject RpcUiClientService _rpcClientService
<div class="modal" id="settingsModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">NVIDIA settings</h5>
@@ -2,7 +2,7 @@
@using ColorControl.Shared.Contracts;
@using ColorControl.Shared.Contracts.Game;
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.Shared.Common;
@using ColorControl.UI.Generics
@@ -52,6 +52,10 @@ else
{
<div class="row mb-3">
<div class="col">
@if (!Presets.Any())
{
<span>No presets found</span>
}
<div class="list-group">
@foreach (var preset in Presets)
{
@@ -66,11 +70,21 @@ else
}
</h5>
</div>
<span>
<span class="d-none d-lg-block">
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Delete</button>
<button class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Clone</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Edit</button>
</span>
<div class="d-lg-none" @onclick:preventDefault @onclick:stopPropagation>
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)">Delete</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)">Clone</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)">Edit</a></li>
</ul>
</div>
</div>
</a>
}
@@ -1,10 +1,10 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@rendermode InteractiveServer
<FieldsModal ModalId="confirmModal" Fields="Fields" Title="@Title" OnConfirm="ConfirmClick"></FieldsModal>
<FieldsModal ModalId="@ModalId" Fields="Fields" Title="@Title" OnConfirm="ConfirmClick"></FieldsModal>
@code {
[Parameter]
@@ -16,6 +16,9 @@
[Parameter]
public Func<Task>? OnConfirm { get; set; }
[Parameter]
public string ModalId { get; set; } = "confirmModal";
public List<FieldDefinition>? Fields { get; set; }
protected override async Task OnParametersSetAsync()
@@ -1,5 +1,5 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@rendermode InteractiveServer
@@ -1,5 +1,5 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@rendermode InteractiveServer
@@ -7,7 +7,7 @@
@inject NavigationManager _navigationManager;
<div class="modal" id="@ModalId" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" @onfocus="ModalFocus">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@Title</h5>
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.EventDispatcher
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
<div class="row mx-0 mb-2">
@@ -0,0 +1,96 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.EventDispatcher
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Services
@rendermode InteractiveServer
@inject JSHelper jsHelper;
@inject NotificationService notificationService;
@inject NavigationManager _navigationManager;
@if (Notifications?.Any() == true)
{
<div aria-live="polite" aria-atomic="true" class="position-relative">
<div class="toast-container bottom-0 end-0 p-3">
@foreach (var notification in Notifications)
{
<div class="toast" id="toast_@notification.Id" role="alert" aria-live="assertive" aria-atomic="true" @onclick="() => NotificationOnClick(notification)">
<div class="toast-header" style="background-color: var(--bs-@notification.Level-bg-subtle)">
<span class="w-100 d-flex">
<div class="alert alert-@notification.Level mb-0 py-1">
<strong class="me-auto">@notification.Category</strong>
</div>
</span>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
@if (notification.InternalUrl != null)
{
<ul class="list-group-flush p-0 mb-0">
<li class="list-group-item">@notification.Message</li>
</ul>
}
else
{
@notification.Message
}
</div>
</div>
}
</div>
</div>
}
@code {
private List<NotificationDto> Notifications { get; } = [];
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
notificationService.OnNotification += AddNotification;
}
}
protected async Task AddNotification(NotificationDto notification)
{
RemoveOldNotifications();
var minShownAt = DateTime.UtcNow.AddSeconds(-5);
// Suppress duplicate noficiations
if (Notifications.Any(n => n.Message == notification.Message && n.Level == notification.Level && n.ShownAt >= minShownAt))
{
return;
}
Notifications.Add(notification);
await InvokeAsync(StateHasChanged);
await jsHelper.ShowToast($"toast_{notification.Id}");
notification.ShownAt = DateTime.UtcNow;
}
private async Task NotificationOnClick(NotificationDto notification)
{
if (notification.InternalUrl != null)
{
_navigationManager.NavigateTo(notification.InternalUrl);
}
Notifications.Remove(notification);
}
private void RemoveOldNotifications()
{
var minDateTime = DateTime.UtcNow.AddMinutes(-1);
Notifications.RemoveAll(n => n.ShownAt.HasValue && n.ShownAt <= minDateTime);
}
}
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.EventDispatcher
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@rendermode InteractiveServer
@@ -3,7 +3,7 @@
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.Shared.EventDispatcher
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@typeparam TPreset where TPreset : PresetBase
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.EventDispatcher
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@rendermode InteractiveServer
+35 -4
View File
@@ -1,13 +1,18 @@
@page "/"
@using ColorControl.Shared.Common
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager;
@inject NotificationService _notificationService
@inject ProtectedSessionStorage _sessionStorage
<PageTitle>Home</PageTitle>
@@ -24,10 +29,10 @@ else
<div class="row">
@foreach (var module in Modules.Where(m => m.IsActive))
{
<div class="col">
<div class="card h-100 mb-3">
<div class="col-lg-4 col-xl mb-3">
<div class="card h-100">
<div class="card-header">
@module.DisplayName
<span class="me-2 bi bi-@module.GetIconClass()" aria-hidden="true"></span>@module.DisplayName
</div>
<div class="card-body">
<a href="@module.Link" class="btn btn-primary stretched-link mb-2">Open module</a>
@@ -47,6 +52,8 @@ else
@code {
private Config? Config;
private List<ModuleDto>? Modules;
private InfoDto? Info;
private bool IsUpdateAvailable;
protected override async Task OnInitializedAsync()
{
@@ -57,4 +64,28 @@ else
Modules.ForEach(m => m.Link = m.DisplayName.Split(' ')[0]);
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender || Info != null || Config?.CheckForUpdates != true)
{
return;
}
var checkedForUpdates = await _sessionStorage.TryGet<bool>("checkedForUpdates");
if (!checkedForUpdates)
{
Info = await _rpcClientService.CallAsync<InfoDto>("OptionsService", "GetInfo");
await _sessionStorage.SetAsync("checkedForUpdates", true);
if (!IsUpdateAvailable && Info?.UpdateInfoDto?.UpdateAvailable == true)
{
IsUpdateAvailable = true;
var message = $"Update available: version {Info.UpdateInfoDto.NewVersionNumber}";
_notificationService.SendNotification(new NotificationDto(message) { InternalUrl = "/info", ScheduledAt = DateTime.UtcNow.AddSeconds(5) });
}
}
}
}
+72 -1
View File
@@ -1,13 +1,14 @@
@page "/info"
@using ColorControl.Shared.Common;
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager;
@inject IHttpContextAccessor _httpContextAccessor
<PageTitle>Logging</PageTitle>
@@ -20,7 +21,41 @@
else
{
<h3 class="mb-2">@info.ApplicationTitle - @info.LegalCopyright</h3>
<div class="d-flex align-items-baseline">
@if (IsUpdateInstalled)
{
if (IsEmbeddedBrowser)
{
<button type="button" class="btn btn-primary me-2" @onclick="RestartOnClick">Restart</button>
}
else
{
<button type="button" class="btn btn-primary me-2" @onclick="ReloadOnClick">Reload</button>
}
<span>New version @info.UpdateInfoDto.NewVersionNumber is installed. Click the button to restart the application.</span>
}
else
{
<button type="button" class="btn btn-primary me-2" disabled="@(!info.UpdateInfoDto.UpdateAvailable || IsInstallingUpdate)" @onclick="InstallUpdateOnClick">
@if (IsInstallingUpdate)
{
<span class="spinner-border spinner-border-sm me-1" aria-hidden="true"></span>
}
<span role="status">Install update</span>
</button>
@if (info.UpdateInfoDto.UpdateAvailable)
{
<span>New version @info.UpdateInfoDto.NewVersionNumber available. Click the button to install the update.</span>
}
else
{
<span>You have the latest version</span>
}
}
</div>
<br/>
<h5>This app contains the following 3rd party plugins:</h5>
<ul class="list-group">
<li class="list-group-item">lgtv.net by gr4b4z</li>
@@ -41,10 +76,46 @@ else
@code {
private InfoDto? info;
private bool IsInstallingUpdate;
private bool IsUpdateInstalled;
private bool IsEmbeddedBrowser;
protected override async Task OnInitializedAsync()
{
info = await _rpcClientService.CallAsync<InfoDto>("OptionsService", "GetInfo");
}
private async Task InstallUpdateOnClick(MouseEventArgs e)
{
IsInstallingUpdate = true;
try
{
var result = await _rpcClientService.CallAsync<bool>("OptionsService", "InstallUpdate");
if (!result)
{
return;
}
IsUpdateInstalled = true;
IsEmbeddedBrowser = _httpContextAccessor.HttpContext?.Request.Headers["user-agent"].ToString().Contains("ColorControlEmbedded") == true;
}
finally
{
IsInstallingUpdate = false;
}
}
private async Task RestartOnClick(MouseEventArgs e)
{
var result = await _rpcClientService.CallAsync<bool>("OptionsService", "RestartAfterUpdate");
}
private async Task ReloadOnClick(MouseEventArgs e)
{
var result = await _rpcClientService.CallAsync<bool>("OptionsService", "RestartAfterUpdate");
_navigationManager.Refresh();
}
}
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.LG
@using ColorControl.Shared.Services
@using ColorControl.UI.Services
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common
@@ -11,7 +11,7 @@
@inject NavigationManager _navigationManager;
<div class="modal" id="deviceModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(Device?.Name == null ? $"Settings for new device" : $"Settings of device {Device.Name}")</h5>
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.LG
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.LG
@using ColorControl.Shared.Services
@using ColorControl.UI.Services
@using ColorControl.UI.Components.Pages.Generic
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common
@@ -13,7 +13,7 @@
@inject NavigationManager _navigationManager;
<div class="modal" id="presetModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" @onfocus="ModalOnFocus">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(Preset?.id == 0 ? $"Settings for new preset" : $"Settings of preset {Preset?.IdOrName}")</h5>
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.LG
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@@ -4,7 +4,7 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.LG
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@@ -155,6 +155,10 @@ else
</div>
<div class="row mb-3">
<div class="col">
@if (!Presets.Any())
{
<span>No presets found</span>
}
<div class="list-group">
@foreach (var preset in Presets)
{
@@ -168,11 +172,21 @@ else
}
<div><small class="text-muted">@preset.shortcut</small></div>
</div>
<span>
<span class="d-none d-lg-block">
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Delete</button>
<button class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Clone</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Edit</button>
</span>
<div class="d-lg-none" @onclick:preventDefault @onclick:stopPropagation>
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)">Delete</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)">Clone</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)">Edit</a></li>
</ul>
</div>
</div>
</a>
}
@@ -215,7 +229,7 @@ else
Devices = await _rpcClientService.CallAsync<List<LgDeviceDto>>("LgService", "GetDevices");
InvokableActions = await _rpcClientService.CallAsync<List<InvokableActionDto<LgPreset>>>("LgService", "GetInvokableActions");
ActionGroups = InvokableActions.GroupBy(a => a.Category).Select(g => new ActionGroup(g.Key, g.ToList())).ToList();
ActionGroups = InvokableActions?.GroupBy(a => a.Category).Select(g => new ActionGroup(g.Key, g.ToList())).ToList() ?? new List<ActionGroup>();
}
protected override async Task OnAfterRenderAsync(bool firstTime)
@@ -1,7 +1,7 @@
@page "/logging"
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@rendermode InteractiveServer
@@ -154,10 +154,13 @@ else
protected override async Task OnInitializedAsync()
{
config = await _rpcClientService.CallAsync<Config>("OptionsService", "GetConfig");
config ??= await _rpcClientService.CallAsync<Config>("OptionsService", "GetConfig");
logLevel = config.LogLevel;
await GetLog();
if (allLines == null)
{
await GetLog();
}
}
private async Task GetLog()
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common;
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common;
@@ -11,7 +11,7 @@
@inject NavigationManager _navigationManager;
<div class="modal modal-lg" id="driverSettingsModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" @onfocus="OnFocus">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(Preset != null ? Preset.IsDisplayPreset ? $"Driver settings for display {Preset.displayName}" : $"Driver settings for {Preset.IdOrName}" : "Driver settings")</h5>
@@ -38,6 +38,12 @@
}
</select>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" @bind="OnlyFoundPrograms" id="onlyFoundPrograms" @bind:after="OnlyFoundProgramsOnChange">
<label class="form-check-label" for="onlyFoundPrograms">
Only show found programs
</label>
</div>
}
<div class="accordion accordion-flush" id="driverSettingsAccordion">
@@ -137,6 +143,8 @@
private bool IsVisible { get; set; }
private bool OnlyFoundPrograms = true;
protected override async Task OnParametersSetAsync()
{
if (_preset != Preset)
@@ -232,12 +240,17 @@
await LoadDriverSettings();
}
private async Task OnlyFoundProgramsOnChange()
{
ProfileNames = await _rpcClientService.CallAsync<List<string>>("NvService", "GetDriverProfileNames", OnlyFoundPrograms);
}
private async Task OnFocus(FocusEventArgs e)
{
if (!IsVisible)
{
IsVisible = true;
ProfileNames ??= await _rpcClientService.CallAsync<List<string>>("NvService", "GetDriverProfileNames");
ProfileNames ??= await _rpcClientService.CallAsync<List<string>>("NvService", "GetDriverProfileNames", OnlyFoundPrograms);
await LoadDriverSettings();
}
}
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.UI.Components.Pages.Generic
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common;
@@ -12,7 +12,7 @@
@inject NavigationManager _navigationManager;
<div class="modal" id="presetModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" @onfocus="ModalOnFocus">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(Preset?.IsDisplayPreset == true ? $"Settings for display {Preset?.displayName}" : Preset?.id == 0 ? $"Settings for new preset" : $"Settings of preset {Preset?.IdOrName}")</h5>
@@ -448,6 +448,7 @@
<div class="mb-2">
<label class="form-label" for="extendedColorimetry">Extended colorimetry</label>
<select class="form-select" id="extendedColorimetry" @bind="Preset.HdmiInfoFrameSettings.ExtendedColorimetry" @oninput="HdmiSettingsOnInput">
<option value="">Not selected</option>
@foreach (var extendedColorimetry in Enum.GetValues<InfoFrameVideoExtendedColorimetry>())
{
<option value="@extendedColorimetry.ToString()">@extendedColorimetry.GetDescription()</option>
@@ -458,6 +459,48 @@
</div>
</div>
}
@if (ColorProfileNames?.Any() == true)
{
<div class="accordion-item">
<h2 class="accordion-header">
<span class="d-flex align-items-baseline">
@if (!Preset.IsDisplayPreset)
{
<input class="form-check-input me-1" style="font-size: 1rem" title="Apply these settings" type="checkbox" @bind="Preset.applyOther" id="applyOther">
}
<button class="accordion-button collapsed py-2 @(Preset.applyOther ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#otherSettings">
Other settings
</button>
</span>
</h2>
<div id="otherSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<label class="form-label" for="colorProfile">Color profile</label>
<select class="form-select" id="colorProfile" @bind="Preset.ColorProfileSettings.ProfileName">
<option value="">Not selected</option>
@foreach (var colorProfile in ColorProfileNames)
{
<option value="@colorProfile">@colorProfile</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-label" for="nvScaling">Scaling</label>
<select class="form-select" id="nvScaling" @bind="Preset.scaling">
<option value="">Not selected</option>
@foreach (var scaling in Enum.GetValues<Scaling>())
{
<option value="@scaling">@scaling.GetDescription()</option>
}
</select>
</div>
</div>
</div>
</div>
}
</div>
}
</form>
@@ -489,6 +532,7 @@
private List<VirtualResolution>? resolutions;
private List<Rational>? refreshRates;
private List<NvPreset>? displayPresets;
private List<string>? ColorProfileNames;
private string WasValidated = "";
private bool IsVisible;
@@ -511,10 +555,17 @@
selectedVirtualResolution = Preset?.DisplayConfig.Resolution?.GetVirtualString();
selectedRefreshRate = Preset?.DisplayConfig.RefreshRate?.ToString();
if (Preset?.IsDisplayPreset != true)
if (Preset?.IsDisplayPreset == false)
{
displayPresets = await _rpcClientService.CallAsync<List<NvPreset>>("NvService", "GetDisplayPresets");
}
var deviceName = Preset?.IsDisplayPreset == true ? Preset.DisplayId : Preset?.DisplayId ?? displayPresets?.FirstOrDefault()?.DisplayId;
if (Preset != null)
{
ColorProfileNames = await _rpcClientService.CallAsync<List<string>>("ColorProfileService", "GetDisplayColorProfiles", deviceName);
}
}
protected override void OnAfterRender(bool firstRender)
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@@ -8,7 +8,7 @@
@inject RpcUiClientService _rpcClientService
<div class="modal" id="settingsModal" tabindex="-1" @onfocus="OnFocus">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">NVIDIA settings</h5>
@@ -2,7 +2,7 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.Shared.Common;
@using ColorControl.UI.Generics
@@ -74,6 +74,21 @@ else
<div class="card-body">
<small class="text-body">@preset.DisplayConfig.Resolution.ToString() @@ @preset.DisplayConfig.RefreshRate.ToString()Hz</small>
<small class="text-muted">@preset.colorData</small>
@if (preset.DitherState != NvDitherState.Auto)
{
<br />
<small class="text-body">Dithering: @preset.DitherState.GetDescription() @preset.NvDitherBits.GetDescription() @preset.NvDitherMode.GetDescription()</small>
}
@if (preset.HDREnabled)
{
<br />
<small class="text-body">HDR enabled @(preset.HdrSettings.OutputMode.HasValue ? $"({preset.HdrSettings.DisplayOutputMode.GetDescription()})" : ""), SDR brightness: @preset.SDRBrightness%</small>
}
@if (!string.IsNullOrEmpty(preset.ColorProfileSettings.ProfileName))
{
<br />
<small class="text-body">Color profile: @preset.ColorProfileSettings.ProfileName</small>
}
</div>
</div>
</div>
@@ -103,10 +118,14 @@ else
</div>
<div class="row mb-3">
<div class="col">
@if (!Presets.Any())
{
<span>No presets found</span>
}
<div class="list-group">
@foreach (var preset in Presets)
{
<a class="list-group-item list-group-item-action" @onclick="() => PresetOnClick(preset)" @onclick:preventDefault @onclick:stopPropagation>
<span class="list-group-item list-group-item-action" @onclick="() => PresetOnClick(preset)" @onclick:preventDefault @onclick:stopPropagation>
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="mb-1">
@@ -117,34 +136,68 @@ else
}
</h5>
</div>
<span>
<span class="d-none d-lg-block">
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Delete</button>
<button class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Clone</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Edit</button>
</span>
<div class="d-lg-none" @onclick:preventDefault @onclick:stopPropagation>
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)">Delete</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)">Clone</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)">Edit</a></li>
</ul>
</div>
</div>
<p class="mb-1">
@preset.DisplayConfig.Resolution.ToString() @@ @preset.DisplayConfig.RefreshRate.ToString()Hz
@(preset.applyColorData ? " " + preset.colorData : "")
@if (preset.ApplyColorEnhancements)
{
<span>, @preset.ColorEnhancementSettings</span>
}
@if (preset.applyHDR)
{
<span>, HDR: @preset.GetHdrDescription()</span>
}
@if (preset.applyDriverSettings)
{
<span>, @preset.driverSettings.Count changed driver settings</span>
}
@if (preset.ApplyNovideoSettings)
{
<span>, @preset.NovideoSettings</span>
}
</p>
<small>
<ul class="list-group list-group-horizontal-md mb-1">
@if (preset.DisplayConfig.ApplyResolution || preset.DisplayConfig.ApplyRefreshRate)
{
<li class="list-group-item p-1">@preset.DisplayConfig.Resolution.ToString() @@ @preset.DisplayConfig.RefreshRate.ToString()Hz</li>
}
@if (preset.applyColorData)
{
<li class="list-group-item p-1">Color settings: @preset.colorData</li>
}
@if (preset.applyDithering)
{
<li class="list-group-item p-1">Dithering: @preset.GetDitheringDescription()</li>
}
@if (preset.ApplyColorEnhancements)
{
<li class="list-group-item p-1">@preset.ColorEnhancementSettings</li>
}
@if (preset.applyHDR)
{
<li class="list-group-item p-1">HDR: @preset.GetHdrDescription()</li>
}
@if (preset.applyDriverSettings)
{
<li class="list-group-item p-1">@preset.driverSettings.Count changed driver settings</li>
}
@if (preset.ApplyNovideoSettings)
{
<li class="list-group-item p-1">@preset.NovideoSettings</li>
}
@if (preset.applyHdmiSettings)
{
<li class="list-group-item p-1">@preset.GetHdmiSettingsDescription()</li>
}
@if (preset.applyOverclocking)
{
<li class="list-group-item p-1">@preset.GetOverclockingSettingsDescription()</li>
}
@if (preset.applyOther && !string.IsNullOrEmpty(preset.ColorProfileSettings.ProfileName))
{
<li class="list-group-item p-1">Color profile: @preset.ColorProfileSettings.ProfileName</li>
}
</ul>
</small>
<small class="text-muted">@preset.shortcut</small>
</a>
</span>
}
</div>
</div>
@@ -1,7 +1,7 @@
@page "/options"
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@@ -9,7 +9,8 @@
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager;
@inject NavigationManager _navigationManager
@inject NotificationService _notificationService
<PageTitle>Options</PageTitle>
@@ -250,6 +251,11 @@ else
{
_navigationManager.NavigateTo("/options", true);
}
else if (module.IsActive)
{
_notificationService.SendNotification(new NotificationDto("Cannot activate the module: check the log for more info", Constants.Danger) { InternalUrl = "/logging" });
module.IsActive = false;
}
}
private async Task PollingIntervalOnInput()
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.Shared.Services
@using ColorControl.UI.Services
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common
@@ -11,7 +11,7 @@
@inject NavigationManager _navigationManager;
<div class="modal" id="deviceModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" @onfocus="ModalOnFocus">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(Device?.Name == null ? $"Add new device" : $"Settings of device {Device.Name}")</h5>
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.Shared.Services
@using ColorControl.UI.Services
@using ColorControl.UI.Components.Pages.Generic
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common
@@ -13,7 +13,7 @@
@inject NavigationManager _navigationManager;
<div class="modal" id="presetModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" @onfocus="ModalOnFocus">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(Preset?.id == 0 ? $"Settings for new preset" : $"Settings of preset {Preset?.IdOrName}")</h5>
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@@ -1,6 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@@ -4,7 +4,7 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.Shared.Services;
@using ColorControl.UI.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@@ -153,6 +153,10 @@ else
</div>
<div class="row mb-3">
<div class="col">
@if (!Presets.Any())
{
<span>No presets found</span>
}
<div class="list-group">
@foreach (var preset in Presets)
{
@@ -166,11 +170,21 @@ else
}
<div><small class="text-muted">@preset.shortcut</small></div>
</div>
<span>
<span class="d-none d-lg-block">
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Delete</button>
<button class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Clone</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Edit</button>
</span>
<div class="d-lg-none" @onclick:preventDefault @onclick:stopPropagation>
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)">Delete</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)">Clone</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)">Edit</a></li>
</ul>
</div>
</div>
</a>
}
+11
View File
@@ -0,0 +1,11 @@
public static class Constants
{
public const string Primary = "primary";
public const string Secondary = "secondary";
public const string Success = "success";
public const string Danger = "danger";
public const string Warning = "warning";
public const string Info = "info";
public const string Light = "light";
public const string Dark = "dark";
}
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using System.Security.Cryptography;
namespace ColorControl.UI.Generics;
public static class GeneralExtensions
{
public static async ValueTask<TValue?> TryGet<TValue>(this ProtectedBrowserStorage storage, string key, TValue? def = default)
{
try
{
var result = await storage.GetAsync<TValue>(key);
var value = result.Success ? result.Value : def;
return value;
}
catch (CryptographicException)
{
await storage.DeleteAsync(key);
return def;
}
}
}
+16 -34
View File
@@ -1,43 +1,25 @@
using ColorControl.Shared.Contracts;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using System.Security.Cryptography;
namespace ColorControl.UI.Generics;
public static class PresetExtensions
{
public static List<T>? OrderPresetsBy<T>(this List<T> presets, PresetOrder order) where T : PresetBase
{
if (presets == null)
{
return null;
}
public static List<T>? OrderPresetsBy<T>(this List<T> presets, PresetOrder order) where T : PresetBase
{
if (presets == null)
{
return null;
}
if (order == PresetOrder.ByName)
{
presets = presets.OrderBy(p => p.name).ToList();
}
else if (order == PresetOrder.ByLastUsed)
{
presets = presets.OrderByDescending(p => p.LastUsed).ToList();
}
if (order == PresetOrder.ByName)
{
presets = presets.OrderBy(p => p.name).ToList();
}
else if (order == PresetOrder.ByLastUsed)
{
presets = presets.OrderByDescending(p => p.LastUsed).ToList();
}
return presets;
}
public static async ValueTask<TValue?> TryGet<TValue>(this ProtectedBrowserStorage storage, string key, TValue? def = default)
{
try
{
var result = await storage.GetAsync<TValue>(key);
var value = result.Success ? result.Value : def;
return value;
}
catch (CryptographicException)
{
await storage.DeleteAsync(key);
return def;
}
}
return presets;
}
}
+26
View File
@@ -0,0 +1,26 @@
using NStandard;
namespace ColorControl.UI;
public class NotificationDto
{
public long Id { get; }
public string Message { get; }
public string Level { get; } = Constants.Info;
public DateTime? ScheduledAt { get; set; }
public string? InternalUrl { get; set; }
public DateTime? ShownAt { get; set; }
public NotificationDto(string message, string level = Constants.Info)
{
Message = message;
Level = level;
Id = Random.Shared.NextInt64();
}
public string Category => Level switch
{
Constants.Danger => "Error",
_ => Level[0].ToUpper() + Level.Substring(1)
};
}
+47
View File
@@ -0,0 +1,47 @@
using Microsoft.JSInterop;
namespace ColorControl.UI.Services;
public class JSHelper(IJSRuntime jsRuntime)
{
private IJSObjectReference? jsModule;
public async Task<bool> IsFormValid(string id)
{
return await InvokeAsync<bool>("FormValid", id);
}
public async Task OpenModal(string id)
{
await InvokeVoidAsync("OpenModal", id);
}
public async Task CloseModal(string id)
{
await InvokeVoidAsync("CloseModal", id);
}
public async Task ShowToast(string id)
{
await InvokeVoidAsync("ShowToast", id);
}
private async ValueTask<T> InvokeAsync<T>(string identifier, params object?[]? names) where T : struct
{
jsModule = await LoadModule();
return await jsModule.InvokeAsync<T>(identifier, names);
}
private async ValueTask InvokeVoidAsync(string identifier, params object?[]? names)
{
jsModule = await LoadModule();
await jsModule.InvokeVoidAsync(identifier, names);
}
private async Task<IJSObjectReference> LoadModule()
{
return jsModule ??= await jsRuntime.InvokeAsync<IJSObjectReference>("import", "./helper.js");
}
}
@@ -0,0 +1,26 @@
namespace ColorControl.UI.Services;
public class NotificationService
{
public event Func<NotificationDto, Task>? OnNotification;
private List<NotificationDto> _scheduledNotifications = new();
public void SendNotification(NotificationDto notification)
{
var now = DateTime.UtcNow;
if (notification.ScheduledAt == null || notification.ScheduledAt <= now)
{
OnNotification?.Invoke(notification);
return;
}
Task.Run(async () =>
{
await Task.Delay(notification.ScheduledAt.Value - now);
OnNotification?.Invoke(notification);
});
}
}
@@ -0,0 +1,89 @@
using ColorControl.Shared.Common;
using ColorControl.Shared.Contracts;
using Newtonsoft.Json;
using System.Diagnostics;
namespace ColorControl.UI.Services;
public class RpcUiClientOptions
{
public int Timeout { get; set; } = PipeUtils.DefaultTimeout;
}
public class RpcUiClientService(NotificationService notificationService)
{
public string? LastErrorMessage { get; private set; }
public async Task<T> CallAsync<T>(string serviceName, string method, params object[] arguments)
{
var rpcMessage = new SvcRpcMessage
{
ServiceName = serviceName,
MethodName = method,
Arguments = arguments
};
var timeout = Debugger.IsAttached ? 60000 : PipeUtils.DefaultTimeout;
return await SendRpcMessageAsync<T>(rpcMessage, timeout, pipeName: PipeUtils.MainPipe);
}
public async Task<T> CallWithOptionsAsync<T>(string serviceName, string method, RpcUiClientOptions options, params object[] arguments)
{
var rpcMessage = new SvcRpcMessage
{
ServiceName = serviceName,
MethodName = method,
Arguments = arguments
};
return await SendRpcMessageAsync<T>(rpcMessage, options.Timeout, pipeName: PipeUtils.MainPipe);
}
public T Call<T>(string serviceName, string method, params object[] arguments)
{
var rpcMessage = new SvcRpcMessage
{
ServiceName = serviceName,
MethodName = method,
Arguments = arguments
};
var timeout = Debugger.IsAttached ? 60000 : 2000;
return PipeUtils.SendRpcMessage<T>(rpcMessage, timeout, pipeName: PipeUtils.MainPipe);
}
private async Task<T> SendRpcMessageAsync<T>(SvcRpcMessage message, int timeout = PipeUtils.DefaultTimeout, string pipeName = PipeUtils.ServicePipe)
{
var messageJson = JsonConvert.SerializeObject(message);
var resultJson = await PipeUtils.SendMessageAsync(messageJson, timeout, pipeName);
if (resultJson == null)
{
notificationService.SendNotification(new NotificationDto("Unknown communication error", Constants.Danger));
return default;
}
var resultMessage = JsonConvert.DeserializeObject<SvcResultMessage>(resultJson);
if (resultMessage?.Data == null)
{
LastErrorMessage = resultMessage?.ErrorMessage ?? "Unknown communication error";
notificationService.SendNotification(new NotificationDto(LastErrorMessage, Constants.Danger));
return default;
}
LastErrorMessage = null;
var result = JsonConvert.DeserializeObject<T>(resultMessage.Data);
return result;
}
}
+3 -3
View File
@@ -42,9 +42,9 @@ h1:focus {
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
-7
View File
@@ -1,7 +0,0 @@
@charset "UTF-8";/*!
* Bootstrap v5.1.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-rgb:33,37,41;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){leLine truncated
/*# sourceMappingURL=bootstrap.min.css.map */
@@ -1 +0,0 @@
{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"iBAAA;;;;;ACAA,MAQI,UAAA,QAAA,YAAA,QAAA,YAAA,QAAA,UAAA,QAAA,SAAA,QAAA,YAAA,QAAA,YAAA,QAAA,WAAA,QAAA,UAAA,QAAA,UAAA,QAAA,WAAA,KAAA,UAAA,QAAA,eAAA,QAIA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAIA,aAAA,QAAA,eAAA,QAAA,aAAA,QAAA,UAAA,QAAA,aAAA,QAAA,YAAA,QAAA,WAAA,QAAA,UAAA,QAIA,iBAAA,EAAA,CAAA,GAAA,CAAA,IAAA,mBAAA,GAAA,CAAA,GAAA,CAAA,IAAA,iBAAA,EAAA,CAAA,GAAA,CAAA,GAAA,cAAA,EAAA,CAAA,GAAA,CAAA,IAAA,iBAAA,GAAA,CAAA,GAAA,CAAA,EAAA,gBAAA,GAAA,CAAA,EAAA,CAAA,GAAA,eAAA,GAAA,CAAA,GAAA,CAAA,IAAA,cAAA,EAAA,CAAA,EAAA,CAAA,GAGF,eAAA,GAAA,CAAA,GAAA,CAAA,IACA,eAAA,CAAA,CAAA,CAAA,CAAA,EACA,cAAA,EAAA,CAAA,EAAA,CAAA,GAMA,qBAAA,SAAA,CAAA,aAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,iBAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBACA,oBAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UACA,cAAA,2EAQA,sBAAA,0BACA,oBAAA,KACA,sBAAA,IACA,sBAAA,IACA,gBAAA,QAIA,aAAA,KClCF,EC+CA,QADA,SD3CE,WAAA,WAeE,8CANJ,MAOM,gBAAA,QAcN,KACE,OAAA,EACA,YAAA,2BEmPI,UAAA,yBFjPJ,YAAA,2BACA,YAAA,2BACA,MAAA,qBACA,WAAA,0BACA,iBAAA,kBACA,yBAAA,KACA,4BAAA,YAUF,GACE,OAAA,KAAA,EACA,MAAA,QACA,iBAAA,aACA,OAAA,EACA,QAAA,IAGF,eACE,OAAA,IAUF,IAAA,IAAA,IAAA,IAAA,IAAA,IAAA,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAGA,YAAA,IACA,YAAA,IAIF,IAAA,GEwMQ,UAAA,uBAlKJ,0BFtCJ,IAAA,GE+MQ,UAAA,QF1MR,IAAA,GEmMQ,UAAA,sBAlKJ,0BFjCJ,IAAA,GE0MQ,UAAA,MFrMR,IAAA,GE8LQ,UAAA,oBAlKJ,0BF5BJ,IAAA,GEqMQ,UAAA,SFhMR,IAAA,GEyLQ,UAAA,sBAlKJ,0BFvBJ,IAAA,GEgMQ,UAAA,QF3LR,IAAA,GEgLM,UAAA,QF3KN,IAAA,GE2KM,UAAA,KFhKN,EACE,WAAA,EACA,cAAA,KCmBF,6BDRA,YAEE,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,iCAAA,KAAA,yBAAA,KAMF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QAMF,GCIA,GDFE,aAAA,KCQF,GDLA,GCIA,GDDE,WAAA,EACA,cAAA,KAGF,MCKA,MACA,MAFA,MDAE,cAAA,EAGF,GACE,YAAA,IAKF,GACE,cAAA,MACA,YAAA,EAMF,WACE,OAAA,EAAA,EAAA,KAQF,ECNA,ODQE,YAAA,OAQF,OAAA,ME4EM,UAAA,OFrEN,MAAA,KACE,QAAA,KACA,iBAAA,QASF,ICpBA,IDsBE,SAAA,SEwDI,UAAA,MFtDJ,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAKN,EACE,MAAA,QACA,gBAAA,UAEA,QACE,MAAA,QAWF,2BAAA,iCAEE,MAAA,QACA,gBAAA,KCxBJ,KACA,ID8BA,IC7BA,KDiCE,YAAA,yBEcI,UAAA,IFZJ,UAAA,IACA,aAAA,cAOF,IACE,QAAA,MACA,WAAA,EACA,cAAA,KACA,SAAA,KEAI,UAAA,OFKJ,SELI,UAAA,QFOF,MAAA,QACA,WAAA,OAIJ,KEZM,UAAA,OFcJ,MAAA,QACA,UAAA,WAGA,OACE,MAAA,QAIJ,IACE,QAAA,MAAA,MExBI,UAAA,OF0BJ,MAAA,KACA,iBAAA,QG7SE,cAAA,MHgTF,QACE,QAAA,EE/BE,UAAA,IFiCF,YAAA,IASJ,OACE,OAAA,EAAA,EAAA,KAMF,ICjDA,IDmDE,eAAA,OAQF,MACE,aAAA,OACA,gBAAA,SAGF,QACE,YAAA,MACA,eAAA,MACA,MAAA,QACA,WAAA,KAOF,GAEE,WAAA,QACA,WAAA,qBCxDF,MAGA,GAFA,MAGA,GDuDA,MCzDA,GD+DE,aAAA,QACA,aAAA,MACA,aAAA,EAQF,MACE,QAAA,aAMF,OAEE,cAAA,EAQF,iCACE,QAAA,ECtEF,OD2EA,MCzEA,SADA,OAEA,SD6EE,OAAA,EACA,YAAA,QE9HI,UAAA,QFgIJLine truncated
+17
View File
@@ -0,0 +1,17 @@
export function FormValid(formId) {
let form = document.getElementById(formId);
return form.checkValidity();
}
export function CloseModal(modalId) {
bootstrap.Modal.getOrCreateInstance('#' + modalId)?.hide();
}
export function OpenModal(modalId) {
bootstrap.Modal.getOrCreateInstance('#' + modalId)?.show();
}
export function ShowToast(toastId) {
const toastBootstrap = bootstrap.Toast.getOrCreateInstance('#' + toastId);
toastBootstrap?.show();
}