add pages for new UI, fix issues

This commit is contained in:
Vincent Maas committed 2024-08-02 13:47:03 +02:00
1 parent 9a9424f1af
commit 4ed670c01b
159 files changed
+18672 -9709

No files matched your search

@@ -0,0 +1,391 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.AMD
@using ColorControl.Shared.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.Shared.Common;
@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-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>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="() => IsVisible = false"></button>
</div>
<div class="modal-body">
<form class="row g-1 needs-validation @WasValidated" novalidate>
@if (Preset == null)
{
<div>Loading...</div>
}
else
{
@if (!Preset.IsDisplayPreset)
{
<div class="mb-2">
<label class="form-label" for="name">Name</label>
<input class="form-control" type="text" @bind="Preset.name" required />
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="Preset.ShowInQuickAccess" id="showInQuickAccess">
<label class="form-check-label" for="showInQuickAccess">
Show in Quick Access
</label>
</div>
<ShortcutInput Label="Shortcut" @bind-Shortcut="Preset.shortcut" />
}
<div class="accordion accordion-flush" id="presetAccordion">
@if (!Preset.IsDisplayPreset)
{
<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; visibility: hidden" type="checkbox">
}
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#displaySettings">
Display
</button>
</span>
</h2>
<div id="displaySettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.primaryDisplay" id="primaryDisplay">
<label class="form-check-label" for="primaryDisplay">
Primary display
</label>
</div>
@if (displayPresets != null)
{
<div class="mb-2">
<label class="form-label" for="mode">Display</label>
<select class="form-select" id="mode" @bind="Preset.DisplayId" disabled="@(Preset.primaryDisplay)">
@foreach (var display in displayPresets)
{
<option value="@display.DisplayId">@display.displayName</option>
}
</select>
</div>
<div class="mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.DisplayConfig.IsPrimary" id="setAsPrimaryDisplay" disabled="@(Preset.primaryDisplay)">
<label class="form-check-label" for="setAsPrimaryDisplay">
Set display as primary display
</label>
</div>
}
</div>
</div>
</div>
}
@if (resolutions != null)
{
<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.DisplayConfig.ApplyResolution" id="applyResolution">
}
<button class="accordion-button py-2 @(Preset.DisplayConfig.ApplyResolution ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#resolutionSettings">
Resolution
</button>
</span>
</h2>
<div id="resolutionSettings" class="accordion-collapse collapse @(Preset.IsDisplayPreset ? "show" : "")" data-bs-parent="#presetAccordion">
<div class="accordion-body">
@if (resolutions != null)
{
<div class="mb-2">
<label class="form-label" for="activeResolution">Active resolution</label>
<select class="form-select" id="activeResolution" @bind="selectedActiveResolution">
@foreach (var resolution in resolutions)
{
<option value="@resolution.GetActiveString()">@resolution.GetActiveString()</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-label" for="virtualResolution">Virtual resolution</label>
<select class="form-select" id="virtualResolution" @bind="selectedVirtualResolution">
@foreach (var resolution in resolutions)
{
<option value="@resolution.GetVirtualString()">@resolution.GetVirtualString()</option>
}
</select>
</div>
}
<div class="mb-2">
<label class="form-label" for="scaling">Scaling/Aspect ratio</label>
<select class="form-select" id="scaling" @bind="Preset.DisplayConfig.Scaling">
@foreach (var scaling in Enum.GetValues<Shared.Native.CCD.DisplayConfigScaling>())
{
<option value="@scaling.ToString()">@scaling.ToString()</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-label" for="rotation">Rotation</label>
<select class="form-select" id="rotation" @bind="Preset.DisplayConfig.Rotation">
@foreach (var rotation in Enum.GetValues<Shared.Native.CCD.DisplayConfigRotation>())
{
<option value="@rotation.ToString()">@rotation.ToString()</option>
}
</select>
</div>
</div>
</div>
</div>
}
@if (refreshRates != null)
{
<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.DisplayConfig.ApplyRefreshRate" id="applyRefreshRate">
}
<button class="accordion-button collapsed py-2 @(Preset.DisplayConfig.ApplyRefreshRate ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#refreshRateSettings">
Refresh Rate
</button>
</span>
</h2>
<div id="refreshRateSettings" class="accordion-collapse collapse show" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<label class="form-label" for="refreshRate">Refresh Rate</label>
<select class="form-select" id="refreshRate" @bind="selectedRefreshRate">
@foreach (var refreshRate in refreshRates)
{
<option value="@refreshRate.ToString()">@refreshRate.ToString() Hz</option>
}
</select>
</div>
</div>
</div>
</div>
}
<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.applyColorData" id="applyColorData">
}
<button class="accordion-button collapsed py-2 @(Preset.applyColorData ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#colorSettings">
Color settings
</button>
</span>
</h2>
<div id="colorSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<label class="form-label" for="bitDepth">Bit depth</label>
<select class="form-select" id="bitDepth" @bind="Preset.colorDepth">
@foreach (var bitDepth in Enum.GetValues<ADLColorDepth>())
{
<option value="@bitDepth.ToString()">@bitDepth.GetDescription()</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-label" for="colorFormat">Format</label>
<select class="form-select" id="colorFormat" @bind="Preset.pixelFormat">
@foreach (var format in Enum.GetValues<ADLPixelFormat>())
{
<option value="@format.ToString()">@format.ToString()</option>
}
</select>
</div>
</div>
</div>
</div>
<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.applyDithering" id="applyDithering">
}
<button class="accordion-button collapsed py-2 @(Preset.applyDithering ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#ditherSettings">
Dithering
</button>
</span>
</h2>
<div id="ditherSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<label class="form-label" for="state">State</label>
<select class="form-select" id="state" @bind="Preset.ditherState">
@foreach (var state in Enum.GetValues<ADLDitherState>())
{
<option value="@state.ToString()">@state.GetDescription()</option>
}
</select>
</div>
</div>
</div>
</div>
<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.applyHDR" id="applyHDR">
}
<button class="accordion-button collapsed py-2 @(Preset.applyHDR ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#hdrSettings">
HDR
</button>
</span>
</h2>
<div id="hdrSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.HDREnabled" id="hdrEnabled">
<label class="form-check-label" for="hdrEnabled">
HDR enabled
</label>
</div>
@if (!Preset.IsDisplayPreset) {
<div class="mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.toggleHDR" id="toggleHdr">
<label class="form-check-label" for="toggleHdr">
Toggle HDR
</label>
</div>
}
<div class="mb-2">
<label class="form-range" for="sdrBrightness">SDR brightness (@Preset.SDRBrightness%)</label>
<input class="form-range" type="range" min="0" max="100" @bind="Preset.SDRBrightness" id="sdrBrightness">
</div>
</div>
</div>
</div>
</div>
}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="() => IsVisible = false">Close</button>
<button type="submit" class="btn btn-primary" data-bs-dismiss="@(IsValid() ? "modal" : "")" @onclick="ApplyClick">@(Preset?.IsDisplayPreset == true ? "Apply" : "Save")</button>
</div>
</div>
</div>
</div>
@code {
public AmdPreset? Preset { get; set; }
[Parameter]
public AmdPreset? PresetParam { get; set; }
[Parameter]
public Func<AmdPreset, Task>? AfterApply { get; set; }
private string? selectedActiveResolution;
private string? selectedVirtualResolution;
private string? selectedRefreshRate;
private List<VirtualResolution>? resolutions;
private List<Rational>? refreshRates;
private List<AmdPreset>? displayPresets;
private string WasValidated = "";
private bool IsVisible;
protected override async Task OnParametersSetAsync()
{
Preset = PresetParam == null ? null : PresetParam.CloneWithId();
if (Preset != null && PresetParam != null)
{
resolutions = await _rpcClientService.CallAsync<List<VirtualResolution>>("AmdService", "GetAvailableResolutionsV2", Preset);
refreshRates = await _rpcClientService.CallAsync<List<Rational>>("AmdService", "GetAvailableRefreshRatesV2", Preset);
}
selectedActiveResolution = Preset?.DisplayConfig.Resolution?.GetActiveString();
selectedVirtualResolution = Preset?.DisplayConfig.Resolution?.GetVirtualString();
selectedRefreshRate = Preset?.DisplayConfig.RefreshRate?.ToString();
if (Preset?.IsDisplayPreset != true)
{
displayPresets = await _rpcClientService.CallAsync<List<AmdPreset>>("AmdService", "GetDisplayPresets");
}
}
private async Task ApplyClick(MouseEventArgs e)
{
if (Preset == null)
{
return;
}
if (!IsValid())
{
WasValidated = "was-validated";
return;
}
var newResolution = resolutions?.FirstOrDefault(r => r.GetActiveString() == selectedActiveResolution);
if (newResolution != null)
{
if (!string.IsNullOrEmpty(selectedVirtualResolution) && selectedActiveResolution != selectedVirtualResolution)
{
newResolution = new VirtualResolution(newResolution.ActiveWidth, newResolution.ActiveHeight);
var parts = selectedVirtualResolution.Split('x');
newResolution.VirtualWidth = uint.Parse(parts[0]);
newResolution.VirtualHeight = uint.Parse(parts[1]);
}
Preset.DisplayConfig.Resolution = newResolution;
}
var newRefreshRate = refreshRates?.FirstOrDefault(r => r.ToString() == selectedRefreshRate);
if (newRefreshRate != null)
{
Preset.DisplayConfig.RefreshRate = newRefreshRate;
}
if (Preset.IsDisplayPreset)
{
//Preset.UpdateAutoApplySettings(PresetParam);
await ApplyDisplayPreset();
return;
}
var result = await _rpcClientService.CallAsync<bool>("AmdService", "UpdatePreset", Preset);
if (AfterApply != null)
{
await AfterApply.Invoke(Preset);
}
}
private async Task ApplyDisplayPreset()
{
var result = await _rpcClientService.CallAsync<bool>("AmdService", "ApplyPreset", Preset);
}
private void ModalOnFocus(FocusEventArgs e)
{
if (!IsVisible)
{
WasValidated = "";
IsVisible = true;
}
}
private bool IsValid()
{
return Preset?.IsDisplayPreset == true || !string.IsNullOrEmpty(Preset?.name);
}
}
@@ -0,0 +1,64 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.AMD
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
<div class="modal" id="settingsModal" tabindex="-1" @onfocus="OnFocus">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">AMD settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="() => Config = null"></button>
</div>
<div class="modal-body">
<form class="row g-1">
@if (Config == null)
{
<div>Loading...</div>
}
else
{
<ShortcutInput Label="Quick Access Shortcut" @bind-Shortcut="Config.QuickAccessShortcut" />
}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="() => Config = null">Close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" @onclick="ApplyClick">Save</button>
</div>
</div>
</div>
</div>
@code {
private AmdServiceConfig? Config { get; set; }
private async Task ApplyClick(MouseEventArgs e)
{
if (Config == null)
{
return;
}
var result = await _rpcClientService.CallAsync<bool>("AmdService", "UpdateConfig", Config);
if (result)
{
Config = null;
}
}
private async Task OnFocus(FocusEventArgs e)
{
if (Config != null)
{
return;
}
Config = await _rpcClientService.CallAsync<AmdServiceConfig>("AmdService", "GetConfig");
}
}
@@ -0,0 +1,208 @@
@page "/AMD"
@using ColorControl.Shared.Common
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.AMD
@using ColorControl.Shared.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject ProtectedLocalStorage _localStorage
<PageTitle>AMD controller</PageTitle>
<AmdSettings />
<div class="d-flex align-items-center">
<h1>AMD controller</h1>
<div class="d-flex ms-auto">
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#settingsModal">
Settings
</button>
</div>
</div>
<h2>Displays</h2>
<AmdPresetPage PresetParam="CurrentPreset" AfterApply="AfterPresetApplied" />
<ConfirmModal Title="Delete preset" Message="Are you sure you want to delete the preset?" OnConfirm="PresetOnDeleteConfirmed" />
@if (DisplayPresets == null)
{
<div>Loading...</div>
}
else
{
<div class="row mb-3">
<div class="col">
@if (!DisplayPresets.Any())
{
<span>No displays found</span>
}
@foreach (var preset in DisplayPresets)
{
<div class="card">
<div class="card-header">
@preset.displayName
@if (preset.DisplayConfig.IsPrimary == true)
{
<small class="text-muted ms-2">Primary</small>
}
<button type="button" class="btn btn-sm btn-primary ms-2" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => DisplayPresetOnClick(preset)">
Settings
</button>
</div>
<div class="card-body">
<small class="text-body">@preset.DisplayConfig.Resolution.ToString() @@ @preset.DisplayConfig.RefreshRate.ToString()Hz</small>
<small class="text-muted">@preset.colorDepth, @preset.pixelFormat</small>
</div>
</div>
}
</div>
</div>
}
@if (Presets == null)
{
<div>Loading...</div>
}
else
{
<div class="mb-2 d-flex align-items-center">
<h2>Presets</h2>
<div class="d-flex ms-auto">
<select class="form-select me-2" id="presetOrder" @bind="PresetOrder" @bind:after="PresetOrderChanged">
@foreach (var order in Enum.GetValues<PresetOrder>())
{
<option value="@order">Order by: @order.GetDescription()</option>
}
</select>
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => AddPresetOnClick()">
Add
</button>
</div>
</div>
<div class="row mb-3">
<div class="col">
<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>
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="mb-1">
@preset.name <small class="text-muted">@(preset.primaryDisplay ? "Primary display" : preset.displayName)</small>
@if (preset.ShowInQuickAccess)
{
<span class="ms-2 badge text-bg-warning" title="Show in Quick Access">QA</span>
}
</h5>
</div>
<span>
<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>
<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>
</a>
}
</div>
</div>
</div>
}
@code {
private List<AmdPreset>? Presets;
private List<AmdPreset>? DisplayPresets;
private List<VirtualResolution>? Resolutions;
private AmdPreset? CurrentPreset;
private PresetOrder PresetOrder;
protected override async Task OnInitializedAsync()
{
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)
{
if (firstTime)
{
PresetOrder = await _localStorage.TryGet<PresetOrder>("AmdPresetOrder");
Presets = Presets?.OrderPresetsBy(PresetOrder);
StateHasChanged();
}
}
private async Task PresetOnClick(AmdPreset preset)
{
await _rpcClientService.CallAsync<bool>("AmdService", "ApplyPresetWithId", preset.id);
}
private async Task PresetOnEditClick(AmdPreset preset)
{
CurrentPreset = preset;
}
private async Task PresetOnCloneClick(AmdPreset preset)
{
CurrentPreset = new AmdPreset(preset);
CurrentPreset.SetCloneValues();
}
private async Task PresetOnDeleteClick(AmdPreset preset)
{
CurrentPreset = preset;
}
private async Task PresetOnDeleteConfirmed()
{
if (CurrentPreset == null)
{
return;
}
await _rpcClientService.CallAsync<bool>("AmdService", "DeletePreset", CurrentPreset.id);
await RefreshPresets();
StateHasChanged();
}
private async Task DisplayPresetOnClick(AmdPreset preset)
{
CurrentPreset = preset;
}
private async Task AddPresetOnClick()
{
CurrentPreset = new AmdPreset();
}
private async Task AfterPresetApplied(AmdPreset preset)
{
await RefreshPresets();
StateHasChanged();
}
private async Task PresetOrderChanged()
{
await _localStorage.SetAsync("AmdPresetOrder", PresetOrder);
await RefreshPresets();
}
private async Task RefreshPresets()
{
Presets = await _rpcClientService.CallAsync<List<AmdPreset>>("AmdService", "GetPresets");
Presets = Presets?.OrderPresetsBy(PresetOrder);
}
}
@@ -1,19 +0,0 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
@@ -0,0 +1,235 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.Game
@using ColorControl.Shared.Services
@using ColorControl.UI.Components.Pages.Generic
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@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-content">
<div class="modal-header">
<h5 class="modal-title">@(Preset?.id == 0 ? $"Settings for new preset" : $"Settings of preset {Preset?.IdOrName}")</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="() => IsVisible = false"></button>
</div>
<div class="modal-body">
<form class="row g-1 needs-validation @WasValidated" novalidate>
@if (Preset == null)
{
<div>Loading...</div>
}
else
{
<div class="mb-2">
<label class="form-label" for="name">Name</label>
<input class="form-control" id="name" type="text" @bind="Preset.name" required />
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.ShowInQuickAccess" id="showInQuickAccess">
<label class="form-check-label" for="showInQuickAccess">
Show in Quick Access
</label>
</div>
<div class="mb-2">
<label class="form-label" for="path">File/URI</label>
<div class="input-group mb-2">
<input class="form-control" id="path" type="text" @bind="Preset.Path" required />
<button type="button" class="btn btn-primary" @onclick="SelectFileOnClick" disabled="@SelectingFile">
<span class="@(SelectingFile ? "spinner-border spinner-border-sm" : "")" aria-hidden="true"></span>
<span role="status">Select...</span>
</button>
</div>
<div id="selectFileHelp" class="form-text">
Selecting a file opens a file dialog outside of the browser on the system where the ColorControl application is running.
</div>
</div>
<div class="mb-2">
<label class="form-label" for="parameters">Parameters</label>
<input class="form-control" id="parameters" type="text" @bind="Preset.Parameters" />
</div>
<div class="accordion accordion-flush" id="presetAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#advancedSettings">
Advanced settings
</button>
</h2>
<div id="advancedSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.RunAsAdministrator" id="runAsAdministrator">
<label class="form-check-label" for="runAsAdministrator">
Run as administrator
</label>
</div>
<FlagsInput Label="Process affinity" Labels="CpuAffinityLabels" @bind-Value="Preset.ProcessAffinityMask"></FlagsInput>
<div class="mb-2">
<label class="form-label" for="priority">Process priority</label>
<select class="form-select" id="priority" @bind="Preset.GameProcessPriorityClass">
@foreach (var priority in Enum.GetValues<GamePriorityClass>())
{
<option value="@priority.ToString()">@priority.GetDescription()</option>
}
</select>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#autoApplySettings">
Auto-apply settings
</button>
</h2>
<div id="autoApplySettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.AutoSettings.AllowAutoApply" id="autoApply">
<label class="form-check-label" for="autoApply">
Automatically execute preset when process starts
</label>
</div>
<div class="mb-2">
<label class="form-label" for="autoApplyAction">Auto-apply action</label>
<select class="form-select" id="priority" @bind="Preset.AutoSettings.ProcessAutoAction">
@foreach (var action in Enum.GetValues<ProcessAutoAction>())
{
<option value="@action.ToString()">@action.GetDescription()</option>
}
</select>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#steps">
Steps
</button>
</h2>
<div id="steps" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<StepsInput Label="Pre-launch" @bind-Steps="Preset.PreLaunchSteps" TPreset="GamePreset"></StepsInput>
<StepsInput Label="Post-launch" @bind-Steps="Preset.PostLaunchSteps" TPreset="GamePreset"></StepsInput>
<StepsInput Label="Finalize" @bind-Steps="Preset.FinalizeSteps" TPreset="GamePreset"></StepsInput>
</div>
</div>
</div>
</div>
}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="() => IsVisible = false">Close</button>
<button type="submit" class="btn btn-primary" data-bs-dismiss="@(IsValid() ? "modal" : "")" @onclick="ApplyClick">Save</button>
</div>
</div>
</div>
</div>
@code {
private GamePreset? Preset;
[Parameter]
public GamePreset? PresetParam { get; set; }
[Parameter]
public Func<GamePreset, Task>? AfterApply { get; set; }
public List<string> CpuAffinityLabels = Enumerable.Range(0, Environment.ProcessorCount).Select(i => $"CPU #{i}").ToList();
public bool SelectingFile { get; set; }
private string WasValidated = "";
private bool IsVisible;
protected override async Task OnParametersSetAsync()
{
if (PresetParam == null)
{
return;
}
Preset = PresetParam.CloneWithId();
}
private async Task ApplyClick(MouseEventArgs e)
{
if (Preset == null)
{
return;
}
if (!IsValid())
{
WasValidated = "was-validated";
return;
}
var result = await _rpcClientService.CallAsync<bool>("GameService", "UpdatePreset", Preset);
if (AfterApply != null)
{
await AfterApply.Invoke(Preset);
}
IsVisible = false;
}
private bool IsValid()
{
return !string.IsNullOrEmpty(Preset?.name) && !string.IsNullOrEmpty(Preset?.Path);
}
private async Task SelectFileOnClick(MouseEventArgs e)
{
if (Preset == null)
{
return;
}
SelectingFile = true;
try
{
var options = new RpcUiClientOptions
{
Timeout = 60000
};
var result = await _rpcClientService.CallWithOptionsAsync<string>("GameService", "SelectPath", options);
if (result != null)
{
Preset.Path = result;
if (string.IsNullOrEmpty(Preset.name))
{
Preset.name = Path.GetFileNameWithoutExtension(result);
}
}
}
finally
{
SelectingFile = false;
}
}
private void ModalOnFocus(FocusEventArgs e)
{
if (!IsVisible)
{
WasValidated = "";
IsVisible = true;
}
}
}
@@ -0,0 +1,61 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.Game
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
<div class="modal" id="settingsModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">NVIDIA settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form class="row g-1">
@if (Config == null)
{
<div>Loading...</div>
}
else
{
<ShortcutInput Label="Quick Access Shortcut" @bind-Shortcut="Config.QuickAccessShortcut" />
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="Config.ApplyExternallyLaunched" id="applyExternallyLaunched">
<label class="form-check-label" for="applyExternallyLaunched">
Apply presets when launching games externally
</label>
</div>
}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" @onclick="ApplyClick">Save</button>
</div>
</div>
</div>
</div>
@code {
private GameServiceConfig? Config { get; set; }
protected override async Task OnInitializedAsync()
{
Config = await _rpcClientService.CallAsync<GameServiceConfig>("GameService", "GetConfig");
}
private async Task ApplyClick(MouseEventArgs e)
{
if (Config == null)
{
return;
}
var result = await _rpcClientService.CallAsync<bool>("GameService", "UpdateConfig", Config);
}
}
@@ -0,0 +1,160 @@
@page "/Game"
@using ColorControl.Shared.Contracts;
@using ColorControl.Shared.Contracts.Game;
@using ColorControl.Shared.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.Shared.Common;
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject ProtectedLocalStorage _localStorage
<PageTitle>Game launcher</PageTitle>
<GameSettings />
<div class="d-flex align-items-center">
<h1>Game launcher</h1>
<div class="d-flex ms-auto">
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#settingsModal">
Settings
</button>
</div>
</div>
<div class="mb-2 d-flex align-items-center">
<h2>Presets</h2>
<div class="d-flex ms-auto">
<select class="form-select me-2" id="presetOrder" @bind="PresetOrder" @bind:after="PresetOrderChanged">
@foreach (var order in Enum.GetValues<PresetOrder>())
{
<option value="@order">Order by: @order.GetDescription()</option>
}
</select>
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => AddPresetOnClick()">
Add
</button>
</div>
</div>
<GamePresetPage PresetParam="CurrentPreset" AfterApply="AfterPresetApplied" />
<ConfirmModal Title="Delete preset" Message="Are you sure you want to delete the preset?" OnConfirm="PresetOnDeleteConfirmed" />
@if (Presets == null)
{
<div>Loading...</div>
}
else
{
<div class="row mb-3">
<div class="col">
<div class="list-group">
@foreach (var preset in Presets)
{
<a class="list-group-item list-group-item-action" @onclick="() => PresetOnClick(preset)">
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="mb-1">
@preset.name
@if (preset.ShowInQuickAccess)
{
<span class="ms-2 badge text-bg-warning" title="Show in Quick Access">QA</span>
}
</h5>
</div>
<span>
<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>
</a>
}
</div>
</div>
</div>
}
@code {
private List<GamePreset>? Presets;
private GamePreset? CurrentPreset;
private PresetOrder PresetOrder;
protected override async Task OnInitializedAsync()
{
await RefreshPresets();
}
protected override async Task OnAfterRenderAsync(bool firstTime)
{
if (firstTime)
{
PresetOrder = await _localStorage.TryGet<PresetOrder>("GamePresetOrder");
Presets = Presets?.OrderPresetsBy(PresetOrder);
StateHasChanged();
}
}
private async Task PresetOnClick(GamePreset preset)
{
await _rpcClientService.CallAsync<bool>("GameService", "ApplyPresetWithId", preset.id);
}
private async Task PresetOnEditClick(GamePreset preset)
{
CurrentPreset = preset;
}
private async Task PresetOnCloneClick(GamePreset preset)
{
CurrentPreset = new GamePreset(preset);
CurrentPreset.SetCloneValues();
}
private async Task PresetOnDeleteClick(GamePreset preset)
{
CurrentPreset = preset;
}
private async Task PresetOnDeleteConfirmed()
{
if (CurrentPreset == null)
{
return;
}
await _rpcClientService.CallAsync<bool>("GameService", "DeletePreset", CurrentPreset.id);
await RefreshPresets();
StateHasChanged();
}
private async Task AddPresetOnClick()
{
CurrentPreset = new GamePreset();
}
private async Task AfterPresetApplied(GamePreset preset)
{
await RefreshPresets();
StateHasChanged();
}
private async Task PresetOrderChanged()
{
await _localStorage.SetAsync("GamePresetOrder", PresetOrder);
await RefreshPresets();
}
private async Task RefreshPresets()
{
Presets = await _rpcClientService.CallAsync<List<GamePreset>>("GameService", "GetPresets");
Presets = Presets?.OrderPresetsBy(PresetOrder);
}
}
@@ -0,0 +1,36 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@rendermode InteractiveServer
<FieldsModal ModalId="confirmModal" Fields="Fields" Title="@Title" OnConfirm="ConfirmClick"></FieldsModal>
@code {
[Parameter]
public string? Title { get; set; }
[Parameter]
public string? Message { get; set; }
[Parameter]
public Func<Task>? OnConfirm { get; set; }
public List<FieldDefinition>? Fields { get; set; }
protected override async Task OnParametersSetAsync()
{
if (Message != null)
{
Fields = [new FieldDefinition { FieldType = FieldType.Label, Label = Message }];
}
}
private async Task ConfirmClick()
{
if (OnConfirm != null)
{
await OnConfirm();
}
}
}
@@ -0,0 +1,125 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@rendermode InteractiveServer
@inject NavigationManager _navigationManager;
@if (Fields == null)
{
return;
}
@foreach (var field in Fields)
{
if (field.FieldType == FieldType.Text)
{
var fieldId = $"fld_{Random.Shared.Next()}";
<div class="mb-2">
<label class="form-label" for="@fieldId">@field.Label</label>
<input class="form-control" type="text" @bind="field.StringValue" id="@fieldId">
@if (field.SubLabel != null)
{
<div id="@(fieldId)_help" class="form-text">
@field.SubLabel
</div>
}
</div>
}
if (field.FieldType == FieldType.Label)
{
var fieldId = $"fld_{Random.Shared.Next()}";
<div class="mb-2">
<label class="form-label" for="@fieldId">@field.Label</label>
@if (field.SubLabel != null)
{
<div id="@(fieldId)_help" class="form-text">
@field.SubLabel
</div>
}
</div>
}
else if (field.FieldType == FieldType.DropDown)
{
var fieldId = $"fld_{Random.Shared.Next()}";
<div class="mb-2">
<label class="form-label" for="@fieldId">@field.Label</label>
<select class="form-select" id="@fieldId" @bind="field.StringValue">
@if (field.ValueType != null)
{
@foreach (var item in Enum.GetValues(field.ValueType))
{
<option value="@item.ToString()">@((item as IConvertible).GetDescription())</option>
}
}
else if (field.Values != null)
{
@foreach (var item in field.Values)
{
<option value="@item.ToString()">@item.ToString()</option>
}
}
</select>
@if (field.SubLabel != null)
{
<div id="@(fieldId)_help" class="form-text">
@field.SubLabel
</div>
}
</div>
}
else if (field.FieldType == FieldType.Shortcut)
{
<ShortcutInput Label="@field.Label" @bind-Shortcut="field.StringValue"></ShortcutInput>
}
else if (field.FieldType == FieldType.CheckBox)
{
var fieldId = $"fld_{Random.Shared.Next()}";
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="field.BoolValue" id="@fieldId">
<label class="form-check-label" for="@fieldId">
@field.Label
</label>
</div>
}
else if (field.FieldType == FieldType.Numeric && (field.MaxValue - field.MinValue) <= 50)
{
var fieldId = $"fld_{Random.Shared.Next()}";
<div class="mb-2">
<label class="form-label" for="@fieldId">@field.Label</label>
<input class="form-control" type="number" @bind="field.IntValue" min="@((int)field.MinValue)" max="@((int)field.MaxValue)" id="@fieldId">
@if (field.SubLabel != null)
{
<div id="@(fieldId)_help" class="form-text">
@field.SubLabel
</div>
}
</div>
}
else if (field.FieldType == FieldType.Numeric && (field.MaxValue - field.MinValue) > 50)
{
var fieldId = $"fld_{Random.Shared.Next()}";
<div class="mb-2">
<label class="form-range" for="@fieldId">@field.Label (Current value: @field.IntValue)</label>
<input class="form-range" type="range" min="@((int)field.MinValue)" max="@((int)field.MaxValue)" @bind="field.IntValue" id="@fieldId">
@if (field.SubLabel != null)
{
<div id="@(fieldId)_help" class="form-text">
@field.SubLabel
</div>
}
</div>
}
}
@code {
[Parameter]
public List<FieldDefinition>? Fields { get; set; }
}
@@ -0,0 +1,127 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@rendermode InteractiveServer
@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-content">
<div class="modal-header">
<h5 class="modal-title">@Title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="OnClose"></button>
</div>
<div class="modal-body">
@if (IsVisible)
{
<FieldDefs Fields="Fields"></FieldDefs>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="OnClose">Cancel</button>
@if (SecondaryCaption != null)
{
<button type="button" class="btn btn-secondary" @onclick="SecondaryClick">@SecondaryCaption</button>
}
<button class="btn btn-primary" type="button" disabled="@IsNextDisabled" data-bs-dismiss="@Dismiss" @onclick="ConfirmClick">
@if (IsNextDisabled)
{
<span class="spinner-border spinner-border-sm me-1" aria-hidden="true"></span>
}
<span role="status">@ConfirmCaption</span>
</button>
</div>
</div>
</div>
</div>
@code {
[Parameter]
public string? ModalId { get; set; }
[Parameter]
public string? ConfirmCaption { get; set; } = "Yes";
[Parameter]
public string? SecondaryCaption { get; set; }
[Parameter]
public string? Dismiss { get; set; } = "modal";
[Parameter]
public string? Title { get; set; }
[Parameter]
public string? Message { get; set; }
[Parameter]
public List<FieldDefinition>? Fields { get; set; }
[Parameter]
public Func<Task>? OnConfirm { get; set; }
[Parameter]
public Func<Task>? OnSecondary { get; set; }
[Parameter]
public Func<Task>? OnShow { get; set; }
private bool IsVisible { get; set; }
private bool IsNextDisabled { get; set; }
private async Task ConfirmClick(MouseEventArgs e)
{
if (OnConfirm == null)
{
return;
}
var dismissing = Dismiss == "modal";
IsNextDisabled = Dismiss == null;
try
{
await OnConfirm();
}
finally
{
IsNextDisabled = false;
}
if (dismissing)
{
IsVisible = false;
Dismiss = "modal";
}
}
private async Task SecondaryClick(MouseEventArgs e)
{
if (OnSecondary == null)
{
return;
}
await OnSecondary();
}
private async Task ModalFocus(FocusEventArgs e)
{
if (!IsVisible)
{
IsVisible = true;
if (OnShow != null)
{
await OnShow();
//StateHasChanged();
}
}
}
private async Task OnClose(MouseEventArgs e)
{
IsVisible = false;
}
}
@@ -0,0 +1,53 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.EventDispatcher
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
<div class="row mx-0 mb-2">
<label class="form-label ps-0">@Label</label>
@if (Labels != null)
{
var index = 1;
var checkValue = 1;
foreach (var label in Labels)
{
var name = $"check{index}";
var checkIndex = index;
<div class="form-check form-check-inline col-@(Col)">
<input class="form-check-input" type="checkbox" role="switch" id="@name" checked="@((Value & checkValue) == checkValue)" @onchange="(ChangeEventArgs e) => CheckOnChange(e, checkIndex)">
<label class="form-check-label" for="@name">@label</label>
</div>
index++;
checkValue *= 2;
}
}
</div>
@code {
[Parameter]
public List<string>? Labels { get; set; }
[Parameter]
public string? Label { get; set; }
[Parameter]
public uint Value { get; set; }
[Parameter]
public EventCallback<uint> ValueChanged { get; set; }
[Parameter]
public int Col { get; set; } = 4;
private async Task CheckOnChange(ChangeEventArgs e, int index)
{
var checkValue = (uint)Math.Pow(2, index - 1);
Value += (uint)((e.Value is bool boolValue && boolValue) ? checkValue : -checkValue);
await ValueChanged.InvokeAsync(Value);
}
}
@@ -0,0 +1,63 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.EventDispatcher
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@rendermode InteractiveServer
@if (Label != null)
{
<div class="mb-2">
<label class="form-label" for="name">@Label</label>
<input class="form-control" type="text" readonly value="@Shortcut"
@onchange="(ChangeEventArgs e) => ShortcutOnChange(e)"
@onkeydown="ShortcutOnKeyDown"
@onkeyup="(KeyboardEventArgs e) => ShortcutOnKeyUp(e)"
@onfocus="ShortcutOnFocus"
@onblur="ShortcutOnBlur"/>
</div>
}
@code {
[Parameter]
public string? Label { get; set; }
[Parameter]
public string? Shortcut { get; set; }
[Parameter]
public EventCallback<string?> ShortcutChanged { get; set; }
private async Task ShortcutOnChange(ChangeEventArgs e)
{
Shortcut = e.Value?.ToString();
await ShortcutChanged.InvokeAsync(Shortcut);
}
private void ShortcutOnKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Tab")
{
return;
}
var text = KeyboardShortcutDispatcher.FormatKeyboardShortcut(e.ShiftKey, e.CtrlKey, e.AltKey, e.Key, e.Code);
Shortcut = text;
}
private async Task ShortcutOnKeyUp(KeyboardEventArgs e)
{
//KeyboardShortcutDispatcher.HandleKeyboardShortcutUp()
await ShortcutChanged.InvokeAsync(Shortcut);
}
private void ShortcutOnFocus(FocusEventArgs e)
{
KeyboardShortcutDispatcher.IsActive = false;
}
private void ShortcutOnBlur(FocusEventArgs e)
{
KeyboardShortcutDispatcher.IsActive = true;
}
}
@@ -0,0 +1,402 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.LG
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.Shared.EventDispatcher
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@typeparam TPreset where TPreset : PresetBase
@inject RpcUiClientService _rpcClientService
@if (Label != null && Steps != null)
{
var index = 0;
<div class="mb-2">
<div class="d-flex align-items-center">
<label class="form-label" for="name">@Label</label>
<div class="d-flex ms-auto">
@if (NvPresets != null || SamsungPresets != null || CustomItems != null)
{
<div class="dropdown dropend mb-1">
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">Add</button>
<form class="dropdown-menu p-4 dropdown-menu-end" style="width: 200px">
@if (NvPresets != null && NvPresetsEnabled)
{
<div class="mb-2">
<label class="form-label" for="nvPreset">NVIDIA preset</label>
<select class="form-select" id="nvPreset" @bind="NvPresetToAddId">
<option value="null">Select preset</option>
@foreach (var preset in NvPresets)
{
<option value="@preset.id">@preset.IdOrName</option>
}
</select>
</div>
}
@if (SamsungPresets != null && SamsungPresetsEnabled)
{
<div class="mb-2">
<label class="form-label" for="samsungPreset">Samsung preset</label>
<select class="form-select" id="samsungPreset" @bind="SamsungPresetToAddId">
<option value="null">Select preset</option>
@foreach (var preset in SamsungPresets)
{
<option value="@preset.id">@preset.IdOrName</option>
}
</select>
</div>
}
@if (LgPresets != null && LgPresetsEnabled)
{
<div class="mb-2">
<label class="form-label" for="lgPreset">LG preset</label>
<select class="form-select" id="lgPreset" @bind="LgPresetToAddId">
<option value="null">Select preset</option>
@foreach (var preset in LgPresets)
{
<option value="@preset.id">@preset.IdOrName</option>
}
</select>
</div>
}
@if (CustomItems != null)
{
foreach (var customItem in CustomItems)
{
<div class="mb-2">
<label class="form-label" for="customItem">@customItem.Name</label>
<select class="form-select" id="customItem" @bind="customItem.SelectedItem">
<option value="null">Select item</option>
@foreach (var item in customItem.Items)
{
<option value="@item">@item</option>
}
</select>
</div>
}
}
@if (ActionItems != null)
{
foreach (var customItem in ActionItems)
{
<div class="mb-2">
<label class="form-label" for="advancedCustomItem">@customItem.Name</label>
<select class="form-select" id="advancedCustomItem" @bind="customItem.SelectedActionName" @bind:after="() => AdvancedItemChanged(customItem)">
<option value="null">Select action</option>
@foreach (var keyValue in customItem.GroupedActions)
{
<optgroup label="@keyValue.Key">
@foreach (var item in keyValue.Value)
{
<option value="@item.Name">@item.Title</option>
}
</optgroup>
}
</select>
</div>
<FieldDefs Fields="ActionFields"></FieldDefs>
}
}
<button type="button" class="btn btn-sm btn-primary" @onclick="() => AddPresetOnClick()">Add items</button>
</form>
</div>
}
</div>
</div>
<nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-home-tab" data-bs-toggle="tab" data-bs-target="#nav-click" type="button" role="tab" aria-controls="nav-click" aria-selected="true">Visual steps</button>
<button class="nav-link" id="nav-profile-tab" data-bs-toggle="tab" data-bs-target="#nav-raw" type="button" role="tab" aria-controls="nav-raw" aria-selected="false">Raw text</button>
</div>
</nav>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-click" role="tabpanel" aria-labelledby="nav-click-tab" tabindex="0">
<div class="btn-group btn-group-sm d-inline-flex flex-wrap" role="group" aria-label="Small button group">
@foreach (var step in PresetSteps)
{
var stepIndex = index;
<button type="button" class="btn btn-outline-primary dropdown-toggle text-break text-wrap" data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">@step.Raw</button>
<form class="dropdown-menu p-4 dropdown-menu-end" style="width: 200px">
<div class="mb-2">
<button type="button" class="btn btn-primary btn-sm" data-bs-dismiss="dropdown" @onclick="() => RemoveOnClick(stepIndex)">Remove</button>
</div>
<div class="mb-2">
<label class="form-label" for="delay">Delay (@(step.Delay)ms)</label>
<input class="form-range" id="delay" type="range" min="0" max="10000" step="50" @bind="step.Delay" @bind:after="() => AfterDelayChanged()">
</div>
</form>
index++;
}
</div>
</div>
<div class="tab-pane fade" id="nav-raw" role="tabpanel" aria-labelledby="nav-raw-tab" tabindex="0">
<textarea class="form-control" id="rawStepText" rows="3" @onchange="RawTextOnChange" @onblur="RawTextOnBlur" value="@RawText"></textarea>
</div>
</div>
</div>
}
@code {
[Parameter]
public string? Label { get; set; }
[Parameter]
public List<string>? Steps { get; set; }
[Parameter]
public EventCallback<List<string>?> StepsChanged { get; set; }
[Parameter]
public List<CustomStepItem>? CustomItems { get; set; }
[Parameter]
public List<ActionStepItem>? ActionItems { get; set; }
[Parameter]
public bool NvPresetsEnabled { get; set; } = true;
[Parameter]
public bool SamsungPresetsEnabled { get; set; } = true;
[Parameter]
public bool LgPresetsEnabled { get; set; } = true;
public class ActionStepItem
{
public string Name { get; private set; }
public List<InvokableActionDto<TPreset>> Actions { get; private set; }
public string? SelectedActionName { get; set; }
public Dictionary<string, List<InvokableActionDto<TPreset>>> GroupedActions;
public ActionStepItem(string name, List<InvokableActionDto<TPreset>> actions)
{
Name = name;
Actions = actions;
GroupedActions = actions.GroupBy(a => a.Category).ToDictionary(k => k.Key, v => v.ToList());
}
public string GetStepString(List<FieldDefinition>? fields)
{
if (string.IsNullOrEmpty(SelectedActionName))
{
return "";
}
var action = Actions.FirstOrDefault(a => a.Name == SelectedActionName);
if (action == null)
{
return "";
}
var field = fields?.FirstOrDefault();
if (field == null || field.Value == null)
{
return $"{action.Name}";
}
return $"{action.Name}({field.Value?.ToString()})";
}
}
private List<NvPreset>? NvPresets { get; set; }
private List<SamsungPreset>? SamsungPresets { get; set; }
private List<LgPreset>? LgPresets { get; set; }
private int? NvPresetToAddId { get; set; }
private int? SamsungPresetToAddId { get; set; }
private int? LgPresetToAddId { get; set; }
private List<PresetStep> PresetSteps = new();
private string? RawText;
private List<FieldDefinition>? ActionFields;
protected override async Task OnParametersSetAsync()
{
if (Steps != null)
{
PresetSteps.Clear();
PresetSteps.AddRange(Steps.Select(s => PresetStep.Parse(s)));
RawText = string.Join(", ", PresetSteps);
}
}
protected override async Task OnInitializedAsync()
{
var config = await _rpcClientService.CallAsync<Config>("OptionsService", "GetConfig");
if (config.HasModule(Module.NvModule))
{
NvPresets = (await _rpcClientService.CallAsync<List<NvPreset>>("NvService", "GetPresets"))?.OrderBy(p => p.name).ToList();
}
if (config.HasModule(Module.SamsungModule))
{
SamsungPresets = (await _rpcClientService.CallAsync<List<SamsungPreset>>("SamsungService", "GetPresets"))?.OrderBy(p => p.name).ToList();
}
if (config.HasModule(Module.LgModule))
{
LgPresets = (await _rpcClientService.CallAsync<List<LgPreset>>("LgService", "GetPresets"))?.OrderBy(p => p.name).ToList();
}
}
private async Task RemoveOnClick(int index)
{
if (PresetSteps.Count > index)
{
PresetSteps.RemoveAt(index);
}
await EmitSteps();
}
private async Task AfterDelayChanged()
{
await EmitSteps();
}
private async Task AddPresetOnClick()
{
AddPreset(NvPresets, "NvPreset", NvPresetToAddId);
AddPreset(SamsungPresets, "SamsungPreset", SamsungPresetToAddId);
AddPreset(LgPresets, "LgPreset", LgPresetToAddId);
if (CustomItems != null && Steps != null)
{
foreach (var customItem in CustomItems)
{
if (string.IsNullOrWhiteSpace(customItem.SelectedItem))
{
continue;
}
PresetSteps.Add(PresetStep.Parse(customItem.SelectedItem));
//customItem.SelectedItem = null;
}
}
if (ActionItems != null && Steps != null)
{
foreach (var customItem in ActionItems)
{
if (string.IsNullOrWhiteSpace(customItem.SelectedActionName))
{
continue;
}
var stepString = customItem.GetStepString(ActionFields);
PresetSteps.Add(PresetStep.Parse(stepString));
//customItem.SelectedItem = null;
}
}
await EmitSteps();
}
private void AddPreset<T>(List<T>? presets, string type, int? id) where T : PresetBase
{
var preset = presets?.FirstOrDefault(p => p.id == id);
if (preset == null || Steps == null || preset.id == 0)
{
return;
}
PresetSteps.Add(PresetStep.Parse($"{type}({preset.name})"));
}
private async Task EmitSteps()
{
if (Steps != null)
{
Steps.Clear();
Steps.AddRange(PresetSteps.Select(ps => ps.UpdatedRaw));
RawText = string.Join(", ", PresetSteps);
await StepsChanged.InvokeAsync(Steps);
}
StateHasChanged();
}
private async Task RawTextOnBlur(FocusEventArgs e)
{
var newSteps = new List<string>();
Utils.ParseWords(newSteps, RawText);
PresetSteps.Clear();
PresetSteps.AddRange(newSteps.Select(s => PresetStep.Parse(s)));
await EmitSteps();
}
private async Task RawTextOnChange(ChangeEventArgs e)
{
RawText = e.Value?.ToString();
}
private void AdvancedItemChanged(ActionStepItem customItem)
{
var action = customItem.Actions.FirstOrDefault(a => a.Name == customItem.SelectedActionName);
if (action == null)
{
ActionFields = null;
return;
}
ActionFields = [CreateField(action)];
}
private FieldDefinition CreateField(InvokableActionDto<TPreset> action)
{
var fieldType = FieldType.Numeric;
var strValue = default(string);
var subLabel = default(string);
var label = action.Title;
if (action.EnumType != null)
{
fieldType = FieldType.DropDown;
strValue = Enum.GetNames(action.EnumType).First();
subLabel = "Note: not all values are available on all models.";
}
else if (action.MinValue == action.MaxValue)
{
fieldType = FieldType.Label;
label = $"This will execute {action.Title}";
}
var field = new FieldDefinition
{
FieldType = fieldType,
Label = label,
SubLabel = subLabel,
ValueType = action.EnumType,
MinValue = action.MinValue,
MaxValue = action.MaxValue,
};
if (fieldType == FieldType.Numeric)
{
field.IntValue = action.CurrentValue;
}
else
{
field.StringValue = strValue;
}
return field;
}
}
@@ -0,0 +1,49 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.EventDispatcher
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@rendermode InteractiveServer
@if (Triggers != null)
{
foreach (var trigger in Triggers)
{
<div class="mb-2">
<label class="form-label" for="triggerType">Trigger type</label>
<select class="form-select" id="triggerType" @bind="trigger.Trigger">
@foreach (var type in Enum.GetValues<PresetTriggerType>())
{
<option value="@type.ToString()">@type.GetDescription()</option>
}
</select>
</div>
<FlagsInput Label="Conditions" Value="@((uint)trigger.Conditions)" Labels="ConditionLabels" Col="6" ValueChanged="ConditionsChanged"></FlagsInput>
<div class="mb-2">
<label class="form-label" for="includedProcesses">Included processes</label>
<input class="form-control" type="text" @bind="trigger.IncludedProcessesAsString" />
</div>
<div class="mb-2">
<label class="form-label" for="excludedProcesses">Excluded processes</label>
<input class="form-control" type="text" @bind="trigger.ExcludedProcessesAsString" />
</div>
}
}
@code {
[Parameter]
public List<PresetTrigger>? Triggers { get; set; }
private List<string> ConditionLabels = Utils.GetDescriptions<PresetConditionType>(fromValue: 1).ToList();
public void ConditionsChanged(uint value)
{
if (Triggers == null || !Triggers.Any())
{
return;
}
var trigger = Triggers.First();
trigger.Conditions = (PresetConditionType)value;
}
}
+55 -2
View File
@@ -1,7 +1,60 @@
@page "/"
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services;
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager;
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
<h1>Color Control</h1>
Welcome to your new app.
<h2>Modules</h2>
@if (Modules == null)
{
<div>Loading...</div>
}
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="card-header">
@module.DisplayName
</div>
<div class="card-body">
<a href="@module.Link" class="btn btn-primary stretched-link mb-2">Open module</a>
<ul>
@foreach (var info in module.Info)
{
<li>@info</li>
}
</ul>
</div>
</div>
</div>
}
</div>
}
@code {
private Config? Config;
private List<ModuleDto>? Modules;
protected override async Task OnInitializedAsync()
{
Config = await _rpcClientService.CallAsync<Config>("OptionsService", "GetConfig");
Modules = await _rpcClientService.CallAsync<List<ModuleDto>>("OptionsService", "GetModules");
if (Modules != null)
{
Modules.ForEach(m => m.Link = m.DisplayName.Split(' ')[0]);
}
}
}
@@ -0,0 +1,50 @@
@page "/info"
@using ColorControl.Shared.Common;
@using ColorControl.Shared.Services;
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager;
<PageTitle>Logging</PageTitle>
<h1>Information</h1>
@if (info == null)
{
<div>Loading...</div>
}
else
{
<h3 class="mb-2">@info.ApplicationTitle - @info.LegalCopyright</h3>
<h5>This app contains the following 3rd party plugins:</h5>
<ul class="list-group">
<li class="list-group-item">lgtv.net by gr4b4z</li>
<li class="list-group-item">Newtonsoft.Json by James Newton-King</li>
<li class="list-group-item">NLog by Jarek Kowalski, Kim Christensen, Julian Verdurmen</li>
<li class="list-group-item">NvAPIWrapper.Net by Soroush Falahati</li>
<li class="list-group-item">NWin32 by zmjack</li>
<li class="list-group-item">TaskScheduler by David Hall</li>
<li class="list-group-item">NVIDIA Profile Inspector by Orbmu2k</li>
<li class="list-group-item">NvidiaML wrapper by LibreHardwareMonitor</li>
<li class="list-group-item">Novideo sRGB by ledoge</li>
<li class="list-group-item">NLogViewer by dojo90</li>
<li class="list-group-item">WPFDarkTheme by AngryCarrot789</li>
<li class="list-group-item">Little-CMS by mm2</li>
<li class="list-group-item">MHC2 by dantmnf</li>
</ul>
}
@code {
private InfoDto? info;
protected override async Task OnInitializedAsync()
{
info = await _rpcClientService.CallAsync<InfoDto>("OptionsService", "GetInfo");
}
}
@@ -0,0 +1,215 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.LG
@using ColorControl.Shared.Services
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager;
<div class="modal" id="deviceModal" tabindex="-1">
<div class="modal-dialog 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>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form class="row g-1">
@if (Device == null)
{
<div>Loading...</div>
}
else
{
<div class="mb-2">
<label class="form-label" for="name">Name</label>
<input class="form-control" id="name" type="text" @bind="Device.Name" />
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="Device.IsCustom" id="isCustom">
<label class="form-check-label" for="isCustom">
Is custom device
</label>
</div>
<div class="mb-2">
<label class="form-label" for="ipAddress">IP-address</label>
<input class="form-control" id="ipAddress" type="text" @bind="Device.IpAddress" disabled="@(!Device.IsCustom)" />
</div>
<div class="mb-2">
<label class="form-label" for="macAddress">MAC-address</label>
<input class="form-control" id="macAddress" type="text" @bind="Device.MacAddress" disabled="@(!Device.IsCustom)" />
</div>
@if (!Device.IsConnected)
{
<div class="mb-2">
<button class="btn btn-primary me-2" type="button" disabled="@IsTestingConnection" @onclick="TestConnectionClick">
@if (IsTestingConnection)
{
<span class="spinner-border spinner-border-sm me-1" aria-hidden="true"></span>
}
<span role="status">Test connection</span>
</button>
@if (IsTestConnectionSuccessful != null)
{
if (IsTestConnectionSuccessful == true)
{
<span class="text-success">Connection test was successful!</span>
}
else
{
<span class="text-danger">Connection failed!</span>
}
}
</div>
}
<div class="accordion accordion-flush" id="accordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#deviceOptions">
Device options
</button>
</h2>
<div id="deviceOptions" class="accordion-collapse collapse" data-bs-parent="#accordion">
<div class="accordion-body">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOnAfterStartup" @bind="Device.Options.PowerOnAfterStartup">
<label class="form-check-label" for="checkPowerOnAfterStartup">Power on after startup</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOnAfterResume" @bind="Device.Options.PowerOnAfterResume">
<label class="form-check-label" for="checkPowerOnAfterResume">Power on after resume from standby</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOffOnShutdown" @bind="Device.Options.PowerOffOnShutdown">
<label class="form-check-label" for="checkPowerOffOnShutdown">Power off on shutdown</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOffOnStandby" @bind="Device.Options.PowerOffOnStandby">
<label class="form-check-label" for="checkPowerOffOnStandby">Power off on standby</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOffOnScreensaverStart" @bind="Device.Options.PowerOffOnScreenSaver">
<label class="form-check-label" for="checkPowerOffOnScreensaverStart">Power off when screensaver activates</label>
<button class="btn btn-sm btn-primary ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#screenSaverSettings" aria-expanded="false" aria-controls="screenSaverSettings">
Settings
</button>
</div>
<div class="collapse mb-3" id="screenSaverSettings">
<div class="card card-body">
<div class="mb-2">
<label class="form-label" for="minScreenSaverDuration">Minimal screen saver duration (seconds)</label>
<input class="form-control" id="minScreenSaverDuration" type="number" @bind="Device.Options.ScreenSaverMinimalDuration" />
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkTurnScreenOffInsteadOfPowerOff" @bind="Device.Options.TurnScreenOffOnScreenSaver">
<label class="form-check-label" for="checkTurnScreenOffInsteadOfPowerOff">Turn screen off instead of power off</label>
<div class="form-text">NOTE: turning the screen off might not work correctly on all models</div>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkTurnScreenOnInsteadOfPowerOn" @bind="Device.Options.TurnScreenOnAfterScreenSaver">
<label class="form-check-label" for="checkTurnScreenOnInsteadOfPowerOn">Turn screen on instead of power on</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPerformActionOnManualScreenSaver" @bind="Device.Options.HandleManualScreenSaver">
<label class="form-check-label" for="checkPerformActionOnManualScreenSaver">Perform action even on manually executed screen saver</label>
</div>
</div>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOnOnScreensaverEnd" @bind="Device.Options.PowerOnAfterScreenSaver">
<label class="form-check-label" for="checkPowerOnOnScreensaverEnd">Power on when screensaver deactivates</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOnAfterManualPowerOff" @bind="Device.Options.PowerOnAfterManualPowerOff">
<label class="form-check-label" for="checkPowerOnAfterManualPowerOff">Power on even after manual power off</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkAllowTriggers" @bind="Device.Options.TriggersEnabled">
<label class="form-check-label" for="checkAllowTriggers">Allow triggers to be fired for this device</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkWindowsPowerOn" @bind="Device.Options.PowerOnByWindows">
<label class="form-check-label" for="checkWindowsPowerOn">Use Windows power settings to power on</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkWindowsPowerOff" @bind="Device.Options.PowerOffByWindows">
<label class="form-check-label" for="checkWindowsPowerOff">Use Windows power settings to power off</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkUseSecureConnection" @bind="Device.Options.UseSecureConnection">
<label class="form-check-label" for="checkUseSecureConnection">Use secure connection</label>
</div>
</div>
</div>
</div>
</div>
}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" @onclick="ApplyClick">Save</button>
</div>
</div>
</div>
</div>
@code {
private LgDeviceDto? _device;
[Parameter]
public LgDeviceDto? Device { get; set; }
[Parameter]
public Func<LgDeviceDto, Task>? AfterApply { get; set; }
private bool IsTestingConnection;
private bool? IsTestConnectionSuccessful;
protected override async Task OnParametersSetAsync()
{
if (_device != Device)
{
_device = Device;
}
}
private async Task ApplyClick(MouseEventArgs e)
{
if (Device == null)
{
return;
}
var result = await _rpcClientService.CallAsync<bool>("LgService", "UpdateDevice", Device);
if (AfterApply != null)
{
await AfterApply.Invoke(Device);
}
}
private async Task TestConnectionClick(MouseEventArgs e)
{
IsTestingConnection = true;
try
{
IsTestConnectionSuccessful = await _rpcClientService.CallWithOptionsAsync<bool>("LgService", "TestDevice", new RpcUiClientOptions { Timeout = 120000 }, Device);
}
finally
{
IsTestingConnection = false;
}
}
}
@@ -0,0 +1,117 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.LG
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
<FieldsModal ModalId="executeActionModal" Fields="Fields" Title="@Title" ConfirmCaption="Apply" OnConfirm="ConfirmClick"></FieldsModal>
@code {
[Parameter]
public InvokableActionDto<LgPreset>? Action { get; set; }
[Parameter]
public List<LgDeviceDto>? Devices { get; set; }
[Parameter]
public Func<Task>? OnConfirm { get; set; }
public string? Title { get; set; }
public List<FieldDefinition>? Fields { get; set; }
private FieldDefinition? Field;
private FieldDefinition? DeviceField;
protected override async Task OnParametersSetAsync()
{
if (Action == null)
{
return;
}
Title = $"Execute action {Action.Title}";
Field = CreateField(Action);
if (Devices?.Any() == true)
{
DeviceField = FieldDefinition.CreateDropDownField("Device", Devices.Select(d => d.ToString()).ToList());
DeviceField.Value = Devices.FirstOrDefault(d => d.IsSelected)?.ToString();
Fields = [DeviceField, Field];
}
else
{
Fields = [Field];
}
}
private FieldDefinition CreateField(InvokableActionDto<LgPreset> action)
{
var fieldType = FieldType.Numeric;
var strValue = default(string);
var subLabel = default(string);
var label = action.Title;
if (action.EnumType != null)
{
fieldType = FieldType.DropDown;
strValue = Enum.GetNames(action.EnumType).First();
subLabel = "Note: not all values are available on all models.";
}
else if (action.MinValue == action.MaxValue)
{
fieldType = FieldType.Label;
label = $"This will execute {action.Title}. Press Apply to continue.";
}
var field = new FieldDefinition
{
FieldType = fieldType,
Label = label,
SubLabel = subLabel,
ValueType = action.EnumType,
MinValue = action.MinValue,
MaxValue = action.MaxValue,
};
if (fieldType == FieldType.Numeric)
{
field.IntValue = action.CurrentValue;
}
else
{
field.StringValue = strValue;
}
return field;
}
private async Task ConfirmClick()
{
if (Action == null || Field == null)
{
return;
}
if (Devices != null && DeviceField != null)
{
Action.SelectedDevice = Devices.FirstOrDefault(d => d.ToString() == DeviceField.Value?.ToString());
}
var parameters = new List<string> { Field.StringValue };
await _rpcClientService.CallAsync<bool>("LgService", "ExecuteInvokableAction", Action, parameters);
if (OnConfirm != null)
{
await OnConfirm();
}
}
}
@@ -0,0 +1,184 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.LG
@using ColorControl.Shared.Services
@using ColorControl.UI.Components.Pages.Generic
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common
@using static ColorControl.UI.Components.Pages.Generic.StepsInput<ColorControl.Shared.Contracts.LG.LgPreset>
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@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-content">
<div class="modal-header">
<h5 class="modal-title">@(Preset?.id == 0 ? $"Settings for new preset" : $"Settings of preset {Preset?.IdOrName}")</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="() => IsVisible = false"></button>
</div>
<div class="modal-body">
<form class="row g-1 needs-validation @WasValidated" novalidate>
@if (Preset == null)
{
<div>Loading...</div>
}
else
{
<div class="mb-2">
<label class="form-label" for="name">Name</label>
<input class="form-control" id="name" type="text" @bind="Preset.name" required />
</div>
@if (Devices != null)
{
<div class="mb-2">
<label class="form-label" for="device">Device</label>
<select class="form-select" id="device" @bind="Preset.DeviceMacAddress">
<option value="">Globally selected device</option>
@foreach (var device in Devices)
{
<option value="@device.MacAddress">@device.Name</option>
}
</select>
</div>
}
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.ShowInQuickAccess" id="showInQuickAccess">
<label class="form-check-label" for="showInQuickAccess">
Show in Quick Access
</label>
</div>
<ShortcutInput Label="Shortcut" @bind-Shortcut="Preset.shortcut" />
<div class="accordion accordion-flush" id="presetAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#advancedSettings">
Advanced settings
</button>
</h2>
<div id="advancedSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<TriggerInput Triggers="Preset.Triggers"></TriggerInput>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#steps">
Steps
</button>
</h2>
<div id="steps" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<StepsInput Label="Steps" @bind-Steps="Preset.steps" CustomItems="CustomItems" ActionItems="CustomActions" LgPresetsEnabled="false" TPreset="LgPreset"></StepsInput>
<div class="mb-2">
<label class="form-label" for="description">Description</label>
<textarea class="form-control" id="description" type="text" @bind="Preset.Description" />
</div>
</div>
</div>
</div>
</div>
}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="() => IsVisible = false">Close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="@(!IsValid() ? "" : "modal")" @onclick="ApplyClick">Save</button>
</div>
</div>
</div>
</div>
@code {
private LgPreset? Preset;
[Parameter]
public LgPreset? PresetParam { get; set; }
[Parameter]
public List<LgDeviceDto>? Devices { get; set; }
[Parameter]
public Func<LgPreset, Task>? AfterApply { get; set; }
private List<CustomStepItem>? CustomItems;
private List<ActionStepItem>? CustomActions;
private string WasValidated = "";
private bool IsVisible;
protected override async Task OnParametersSetAsync()
{
if (PresetParam == null)
{
return;
}
Preset = PresetParam.CloneWithId();
}
protected override async Task OnInitializedAsync()
{
CustomItems = [new CustomStepItem("RC buttons", Enum.GetValues<ButtonType>().Select(b =>
{
var result = b.ToString();
if (result[0] == '_')
{
result = result.Substring(1);
}
return result;
}).ToList())];
var actions = await _rpcClientService.CallAsync<List<InvokableActionDto<LgPreset>>>("LgService", "GetInvokableActions");
if (actions != null)
{
CustomActions = [new ActionStepItem("Actions", actions.Where(a => !a.Advanced).ToList())];
}
}
private async Task ApplyClick(MouseEventArgs e)
{
if (Preset == null)
{
return;
}
if (!IsValid())
{
WasValidated = "was-validated";
return;
}
var result = await _rpcClientService.CallAsync<bool>("LgService", "UpdatePreset", Preset);
if (AfterApply != null)
{
await AfterApply.Invoke(Preset);
}
}
private void ModalOnFocus(FocusEventArgs e)
{
if (!IsVisible)
{
WasValidated = "";
IsVisible = true;
}
}
private bool IsValid()
{
return !string.IsNullOrEmpty(Preset?.name);
}
}
@@ -0,0 +1,34 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.LG
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
<FieldsModal Title="Lg settings" ConfirmCaption="Save" ModalId="settingsModal" Fields="Fields" OnConfirm="ApplyClick" OnShow="ModalOnShow"></FieldsModal>
@code {
private LgServiceConfigDto? Config { get; set; }
private List<FieldDefinition>? Fields { get; set; }
protected async Task ModalOnShow()
{
Config = await _rpcClientService.CallAsync<LgServiceConfigDto>("LgService", "GetConfig");
Fields = LgFields.GetSettingsFields(Config);
StateHasChanged();
}
private async Task ApplyClick()
{
if (Config == null)
{
return;
}
FieldDefinition.UpdateObject(Fields, Config);
var result = await _rpcClientService.CallAsync<bool>("LgService", "UpdateConfig", Config);
}
}
@@ -0,0 +1,353 @@
@page "/LG"
@using ColorControl.Shared.Common
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.LG
@using ColorControl.Shared.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject ProtectedLocalStorage _localStorage
<PageTitle>LG controller</PageTitle>
<LgSettings />
<LgExecuteActionModal Action="@CurrentInvokableAction" Devices="Devices" />
<div class="d-flex align-items-center">
<h1>LG controller</h1>
<div class="d-flex ms-auto">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle me-2" data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">
Buttons
</button>
<ul class="dropdown-menu">
@foreach (var button in Enum.GetValues<ButtonType>())
{
<li>
<a class="dropdown-item" @onclick="() => ButtonOnClick(button)">@button.GetDescription()</a>
</li>
}
</ul>
</div>
@if (ActionGroups != null)
{
<div class="btn-group">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle me-2" data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">
Expert
</button>
<ul class="dropdown-menu">
@foreach (var group in ActionGroups)
{
<li class="dropstart">
<a class="dropdown-item dropdown-toggle" data-bs-toggle="dropdown" href="#">@group.Name</a>
<ul class="dropdown-menu dropdown-submenu">
@foreach (var action in group.Actions)
{
<li><a class="dropdown-item" @onclick="() => ActionOnclick(action)" data-bs-toggle="modal" data-bs-target="#executeActionModal">@action.Title</a></li>
}
</ul>
</li>
}
</ul>
</div>
}
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#settingsModal">
Settings
</button>
</div>
</div>
<LgDevicePage Device="CurrentDevice" />
<LgPresetPage PresetParam="CurrentPreset" Devices="Devices" AfterApply="AfterPresetApplied" />
<ConfirmModal Title="Delete preset" Message="Are you sure you want to delete the preset?" OnConfirm="PresetOnDeleteConfirmed" />
@if (Devices == null)
{
<div>Loading...</div>
}
else
{
<div class="mb-2 d-flex align-items-center">
<h2>Devices</h2>
<div class="d-flex ms-auto">
<button type="button" class="btn btn-sm btn-primary me-2" disabled="@IsRefreshDisabled" @onclick="RefreshDevicesOnClick">
@if (IsRefreshDisabled)
{
<span class="spinner-border spinner-border-sm me-1" aria-hidden="true"></span>
}
<span role="status">Refresh</span>
</button>
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#deviceModal" @onclick="() => AddDeviceOnClick()">
Add
</button>
</div>
</div>
<div class="row mb-3">
@if (!Devices.Any())
{
<span>No devices found</span>
}
@foreach (var device in Devices)
{
<div class="col-lg-6 mb-3">
<div class="card h-100">
<div class="card-header d-flex align-items-baseline">
<span>
@device.Name
@if (device.IsConnected)
{
<span class="ms-2 badge text-bg-success" title="Device is connected">Connected</span>
}
else
{
<span class="ms-2 badge text-bg-danger" title="Device is disconnected">Disconnected</span>
}
@if (device.IsSelected)
{
<span class="ms-2 badge text-bg-primary" title="Global device">Primary</span>
}
</span>
<div class="d-flex ms-auto">
@if (!device.IsSelected)
{
<button class="btn btn-sm btn-secondary ms-2" @onclick="() => SetAsPrimaryOnClick(device)">Set as primary</button>
}
<button type="button" class="btn btn-sm btn-primary ms-2" data-bs-toggle="modal" data-bs-target="#deviceModal" @onclick="() => DeviceOnClick(device)">
Settings
</button>
</div>
</div>
<div class="card-body">
<small class="text-muted">@device.IpAddress (@(device.MacAddress ?? "No MAC-address entered"))</small>
</div>
</div>
</div>
}
</div>
}
@if (Presets == null)
{
<div>Loading...</div>
}
else
{
<div class="mb-2 d-flex align-items-center">
<h2>Presets</h2>
<div class="d-flex ms-auto">
<select class="form-select me-2" id="presetOrder" @bind="PresetOrder" @bind:after="PresetOrderChanged">
@foreach (var order in Enum.GetValues<PresetOrder>())
{
<option value="@order">Order by: @order.GetDescription()</option>
}
</select>
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => AddPresetOnClick()">
Add
</button>
</div>
</div>
<div class="row mb-3">
<div class="col">
<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>
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="mb-1">@preset.name <small class="text-muted">@GetDeviceName(preset)</small></h5>
@if (preset.Triggers.Any(t => t.Trigger != PresetTriggerType.None))
{
<div><small class="text-muted">Trigger: @preset.Triggers[0].ToString()</small></div>
}
<div><small class="text-muted">@preset.shortcut</small></div>
</div>
<span>
<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>
</a>
}
</div>
</div>
</div>
}
@code {
private class ActionGroup
{
public string Name { get; }
public List<InvokableActionDto<LgPreset>> Actions { get; }
public ActionGroup(string name, List<InvokableActionDto<LgPreset>> actions)
{
Name = (name ?? "Misc");
Actions = actions;
}
}
private List<LgPreset>? Presets;
private List<LgDeviceDto>? Devices;
private List<InvokableActionDto<LgPreset>>? InvokableActions;
private List<ActionGroup>? ActionGroups;
private LgServiceConfigDto? Config;
private LgPreset? CurrentPreset;
private LgDeviceDto? CurrentDevice;
private bool IsRefreshDisabled;
private InvokableActionDto<LgPreset>? CurrentInvokableAction;
private PresetOrder PresetOrder;
protected override async Task OnInitializedAsync()
{
Config = await _rpcClientService.CallAsync<LgServiceConfigDto>("LgService", "GetConfig");
await RefreshPresets();
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();
}
protected override async Task OnAfterRenderAsync(bool firstTime)
{
if (firstTime)
{
PresetOrder = await _localStorage.TryGet<PresetOrder>("LgPresetOrder");
Presets = Presets?.OrderPresetsBy(PresetOrder);
StateHasChanged();
}
}
private async Task PresetOnClick(LgPreset preset)
{
await _rpcClientService.CallAsync<bool>("LgService", "ApplyPresetWithId", preset.id);
}
private async Task PresetOnEditClick(LgPreset preset)
{
CurrentPreset = preset;
}
private async Task PresetOnCloneClick(LgPreset preset)
{
CurrentPreset = new LgPreset(preset);
CurrentPreset.SetCloneValues();
}
private async Task PresetOnDeleteClick(LgPreset preset)
{
CurrentPreset = preset;
}
private async Task PresetOnDeleteConfirmed()
{
if (CurrentPreset == null)
{
return;
}
await _rpcClientService.CallAsync<bool>("LgService", "DeletePreset", CurrentPreset.id);
await RefreshPresets();
StateHasChanged();
}
private async Task DeviceOnClick(LgDeviceDto device)
{
CurrentDevice = device;
}
private async Task AddDeviceOnClick()
{
CurrentDevice = new LgDeviceDto { IsCustom = true };
}
private async Task AddPresetOnClick()
{
CurrentPreset = new LgPreset();
}
private async Task AfterPresetApplied(LgPreset preset)
{
await RefreshPresets();
StateHasChanged();
}
private async Task RefreshDevicesOnClick(MouseEventArgs e)
{
IsRefreshDisabled = true;
try
{
try
{
await _rpcClientService.CallAsync<bool>("LgService", "RefreshDevices");
}
catch
{
}
Devices = await _rpcClientService.CallAsync<List<LgDeviceDto>>("LgService", "GetDevices");
}
finally
{
IsRefreshDisabled = false;
}
StateHasChanged();
}
private async Task ActionOnclick(InvokableActionDto<LgPreset> action)
{
CurrentInvokableAction = action;
}
private string GetDeviceName(LgPreset preset)
{
if (string.IsNullOrEmpty(preset.DeviceMacAddress))
{
return "Global";
}
return Devices?.FirstOrDefault(d => d.MacAddress == preset.DeviceMacAddress)?.Name ?? preset.DeviceMacAddress;
}
private async Task SetAsPrimaryOnClick(LgDeviceDto device)
{
var result = await _rpcClientService.CallAsync<bool>("LgService", "SetSelectedDevice", device);
if (result)
{
Devices = await _rpcClientService.CallAsync<List<LgDeviceDto>>("LgService", "GetDevices");
}
}
private async Task PresetOrderChanged()
{
await _localStorage.SetAsync("LgPresetOrder", PresetOrder);
await RefreshPresets();
}
private async Task RefreshPresets()
{
Presets = await _rpcClientService.CallAsync<List<LgPreset>>("LgService", "GetPresets");
Presets = Presets?.OrderPresetsBy(PresetOrder);
}
private async Task ButtonOnClick(ButtonType button)
{
var preset = new LgPreset(button.ToString(), null, [button.ToString()]);
await _rpcClientService.CallAsync<bool>("LgService", "ApplyPreset", preset);
}
}
@@ -0,0 +1,205 @@
@page "/logging"
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services;
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager;
<PageTitle>Logging</PageTitle>
<h1>Logging</h1>
@if (config == null)
{
<div>Loading...</div>
}
else
{
<div class="row mb-3">
<div class="col-lg-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">Settings</h5>
</div>
<form class="ms-3 me-3">
<div class="mb-3 col-md-3">
<label class="form-label" for="logLevel">
Log Level
</label>
<select class="form-select" @bind="logLevel" id="logLevel" @oninput="LogLevelInput">
@foreach (var level in logLevels)
{
<option value="@level.Name">@level.Name</option>
}
</select>
</div>
</form>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-3">
<label class="form-label" for="logSource">
Show log from
</label>
<select class="form-select" @bind="logSource" id="logSource" @bind:after="GetLog">
<option value="0">Main application</option>
<option value="1">Service</option>
</select>
</div>
<div class="col-3">
<label class="form-label" for="logOrder">
Order
</label>
<select class="form-select" @bind="logOrder" id="logOrder" @bind:after="GetLog">
<option value="0">Newest first</option>
<option value="1">Oldest first</option>
</select>
</div>
<div class="col-3">
<label class="form-label" for="maxLines">
Maximum number of lines to show
</label>
<select class="form-select" @bind="maxLines" id="maxLines" @bind:after="GetLog">
<option value="1000">1000</option>
<option value="10000">10000</option>
<option value="999999999">Show all</option>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-12">
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th scope="col">TimeStamp</th>
<th scope="col">
<select class="form-select fw-bold" id="logLevel" @oninput="LogLevelFilterInput">
@foreach (var level in logLevels)
{
<option class="fw-bold" value="@level.Name">@level.Name</option>
}
</select>
</th>
<th scope="col">Name</th>
<th scope="col">Message</th>
</tr>
</thead>
<tbody>
@foreach (var logLine in logLines.Where(l => l.Show(logLevelFilter)))
{
<tr class="table-@logLine.BsClass">
<td style="min-width: 220px">@logLine.TimeStamp</td>
<td style="min-width: 120px">@logLine.Level</td>
<td>@logLine.Name</td>
<td>@logLine.Message</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
@code {
private Config? config;
private string logLevel = "0";
private int logSource = 0;
private int maxLines = 1000;
private int logOrder = 0;
private List<string>? allLines;
private List<LogLine> logLines = new();
private string logLevelFilterName = NLog.LogLevel.Debug.Name;
private NLog.LogLevel logLevelFilter = NLog.LogLevel.Debug;
private List<NLog.LogLevel> logLevels = [NLog.LogLevel.Trace, NLog.LogLevel.Debug, NLog.LogLevel.Info, NLog.LogLevel.Warn, NLog.LogLevel.Error, NLog.LogLevel.Fatal];
private class LogLine
{
public string TimeStamp { get; }
public string Level { get; }
public string Name { get; }
public string Message { get; }
public NLog.LogLevel LogLevel { get; }
public LogLine(string timeStamp, string level, string name, string message)
{
TimeStamp = timeStamp;
Level = level;
Name = name;
Message = message;
LogLevel = NLog.LogLevel.FromString(level);
}
public string BsClass => Level switch
{
"TRACE" => "dark",
"DEBUG" => "secondary",
"INFO" => "info",
"WARN" => "warning",
"ERROR" => "danger",
"FATAL" => "secondary",
};
public bool Show(NLog.LogLevel level) => LogLevel >= level;
}
protected override async Task OnInitializedAsync()
{
config = await _rpcClientService.CallAsync<Config>("OptionsService", "GetConfig");
logLevel = config.LogLevel;
await GetLog();
}
private async Task GetLog()
{
allLines = await _rpcClientService.CallAsync<List<string>>("LoggingService", "LoadLog", logSource, maxLines, logOrder);
ProcessLines();
}
private void ProcessLines()
{
logLines.Clear();
if (allLines == null)
{
return;
}
var lastLogLine = default(LogLine);
foreach (var line in allLines)
{
var strings = line.Split('|');
if (strings.Count() == 4)
{
lastLogLine = new LogLine(strings[0], strings[1], strings[2], strings[3]);
logLines.Add(lastLogLine);
}
}
}
private async Task LogLevelInput(ChangeEventArgs e)
{
var value = e.Value ?? NLog.LogLevel.Debug.Name;
await _rpcClientService.CallAsync<bool>("LoggingService", "SetLogLevel", value);
}
private async Task LogLevelFilterInput(ChangeEventArgs e)
{
var value = e.Value as string ?? NLog.LogLevel.Debug.Name;
logLevelFilter = NLog.LogLevel.FromString(value);
}
}
@@ -0,0 +1,181 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Services;
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common;
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager;
<div class="modal modal-lg" id="ditheringModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Test dithering</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form class="row g-1">
<div class="row">
@if (displayPresets != null)
{
<div class="mb-2">
<label class="form-label" for="mode">Display</label>
<select class="form-select" id="mode" @bind="SelectedDisplayId" disabled="@(displayPresets.Count <= 1)">
@foreach (var display in displayPresets)
{
<option value="@display.DisplayId">@display.displayName</option>
}
</select>
</div>
}
</div>
@if (Preset == null)
{
<div>Loading...</div>
}
else
{
<div class="row">
<div class="mb-2">
<label class="form-label" for="state">State</label>
<select class="form-select" id="state" @bind="Preset.NvDitherState" @bind:after="ApplyNormalPreset">
@foreach (var state in Enum.GetValues<NvDitherState>())
{
<option value="@state.ToString()">@state.GetDescription()</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-label" for="mode">Mode</label>
<select class="form-select" id="mode" @bind="Preset.NvDitherMode" @bind:after="ApplyNormalPreset">
@foreach (var mode in Enum.GetValues<NvDitherMode>())
{
<option value="@mode.ToString()">@mode.GetDescription()</option>
}
</select>
</div>
<div class="mb-3">
<label class="form-label" for="bits">Bit depth</label>
<select class="form-select" id="bits" @bind="Preset.NvDitherBits" @bind:after="ApplyNormalPreset">
@foreach (var bits in Enum.GetValues<NvDitherBits>())
{
<option value="@bits.ToString()">@bits.GetDescription()</option>
}
</select>
</div>
</div>
<div class="">
<div class="btn-group ms-2 mb-2">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle me-2" data-bs-toggle="dropdown" aria-expanded="false">
Gradient
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" @onclick="() => GradientColor = GrayGradient">Grayscale</a></li>
<li><a class="dropdown-item" @onclick="() => GradientColor = RedGradient">Red</a></li>
<li><a class="dropdown-item" @onclick="() => GradientColor = GreenGradient">Green</a></li>
<li><a class="dropdown-item" @onclick="() => GradientColor = BlueGradient">Blue</a></li>
</ul>
</div>
<div class="mb-2 col-24 mx-2" style="height: 200px; background: linear-gradient(to right, #000000 0%, @GradientColor 100%)">
</div>
</div>
}
</form>
</div>
<div class="modal-footer">
<span class="me-auto">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="SetDitherRegistryKeyOnClick">Set dither registry key</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="RestartDriverOnClick">Restart driver</button>
</span>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
@code {
public const string GrayGradient = "#ffffff";
public const string RedGradient = "#ff0000";
public const string GreenGradient = "#00ff00";
public const string BlueGradient = "#0000ff";
private bool _visible;
[Parameter]
public bool Visible { get; set; }
public NvPreset? Preset { get; set; }
private string? SelectedDisplayId { get; set; }
private List<NvPreset>? displayPresets;
private string GradientColor { get; set; } = GrayGradient;
protected override async Task OnParametersSetAsync()
{
if (_visible != Visible)
{
_visible = Visible;
if (Visible)
{
await GetSettingsAsync();
}
}
}
protected async Task GetSettingsAsync()
{
displayPresets = await _rpcClientService.CallAsync<List<NvPreset>>("NvService", "GetDisplayPresets");
if (displayPresets?.Any() == true)
{
Preset = displayPresets.FirstOrDefault(d => d.DisplayConfig.IsPrimary == true) ?? displayPresets.First();
SelectedDisplayId = Preset.DisplayId;
}
}
private async Task DitherOnChange(EventArgs e)
{
if (Preset == null)
{
return;
}
Preset.applyDithering = true;
await _rpcClientService.CallAsync<bool>("NvService", "ApplyPresetWithId", Preset.id);
}
private async Task ApplyNormalPreset()
{
await ApplyPreset();
}
private async Task SetDitherRegistryKeyOnClick(MouseEventArgs e)
{
await ApplyPreset(true);
}
private async Task RestartDriverOnClick(MouseEventArgs e)
{
await ApplyPreset(true, true);
}
private async Task ApplyPreset(bool setRegistryKey = false, bool restartDriver = false)
{
if (Preset == null)
{
return;
}
Preset.applyDithering = true;
Preset.SetDitherRegistryKey = setRegistryKey;
Preset.RestartDriver = restartDriver;
await _rpcClientService.CallAsync<bool>("NvService", "ApplyPreset", Preset);
}
}
@@ -0,0 +1,247 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Services;
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common;
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@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-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>
@if (Preset == null)
{
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="() => IsVisible = false"></button>
}
else
{
<button type="button" class="btn-close" data-bs-toggle="modal" data-bs-target="#presetModal" aria-label="Close" @onclick="() => IsVisible = false"></button>
}
</div>
<div class="modal-body">
<form class="row g-1">
@if (ProfileNames != null)
{
<div class="mb-2">
<label class="form-label" for="profileName">Profile</label>
<select class="form-select" id="profileName" @bind="CurrentProfileName" @bind:after="ProfileOnChange">
@foreach (var profileName in ProfileNames)
{
<option value="@profileName">@profileName</option>
}
</select>
</div>
}
<div class="accordion accordion-flush" id="driverSettingsAccordion">
@if (Groups != null)
{
foreach (var group in Groups)
{
var groupId = $"{group.Name.Replace(' ', '_')}Settings";
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed py-2 @(group.HasChangedSettings ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#@groupId">
@group.Name
</button>
</h2>
<div id="@groupId" class="accordion-collapse collapse" data-bs-parent="#driverSettingsAccordion">
<div class="accordion-body">
@foreach (var setting in group.DriverSettings)
{
if (NvSettingConstants.RangeDriverSettings.Contains(setting.SettingId) && setting.Values != null)
{
<div class="mb-2">
<label class="form-label @(setting.Value != setting.DefaultValue && setting.Value != NvSettingConstants.UnsetDwordValue ? "text-warning fw-bold" : "")" for="@setting.SettingId">@setting.SettingText (@setting.Value)</label>
<input class="form-range" type="range" min="@setting.Values.Min(v => v.Value)" max="@setting.Values.Max(v => v.Value)" @bind="setting.Value" id="@setting.SettingId">
</div>
}
else if (setting.Values != null)
{
<div class="mb-2">
<label class="form-label @(setting.Value != setting.DefaultValue && setting.Value != NvSettingConstants.UnsetDwordValue ? "text-warning fw-bold" : "")" for="@setting.SettingId">@setting.SettingText</label>
<select class="form-select" id="@setting.SettingId" @bind="setting.Value">
@if (Preset != null)
{
<option value="@(NvSettingConstants.UnsetDwordValue)">Unchanged</option>
}
@foreach (var settingValue in setting.Values)
{
<option value="@settingValue.Value" class="@(setting.DefaultValue == settingValue.Value ? "text-success" : "")">@settingValue.ValueName</option>
}
</select>
</div>
}
}
</div>
</div>
</div>
}
}
</div>
</form>
</div>
<div class="modal-footer">
@if (Preset == null)
{
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="() => IsVisible = false">Close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" @onclick="ApplyClick">Apply</button>
}
else
{
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => IsVisible = false">Close</button>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="ApplyClick">Save</button>
}
</div>
</div>
</div>
</div>
@code {
public class GroupedSettings
{
public string Name { get; private set; }
public List<NvSettingItemDto> DriverSettings { get; private set; }
public bool HasChangedSettings { get; private set; }
public GroupedSettings(string name, List<NvSettingItemDto> settings, bool hasChangedSettings)
{
Name = name;
DriverSettings = settings;
HasChangedSettings = hasChangedSettings;
}
}
private NvPreset? _preset;
[Parameter]
public NvPreset? Preset { get; set; }
[Parameter]
public Func<NvPreset, Task>? AfterApply { get; set; }
private List<GroupedSettings>? Groups { get; set; }
private List<NvSettingItemDto>? DriverSettings { get; set; }
private List<string>? ProfileNames { get; set; }
private string? CurrentProfileName { get; set; }
private bool IsVisible { get; set; }
protected override async Task OnParametersSetAsync()
{
if (_preset != Preset)
{
_preset = Preset;
CurrentProfileName = Preset?.DriverProfileName;
DriverSettings = null;
await LoadDriverSettings();
}
}
protected async Task LoadDriverSettings()
{
if (DriverSettings == null)
{
DriverSettings = await _rpcClientService.CallAsync<List<NvSettingItemDto>>("NvService", "GetDriverSettings", CurrentProfileName);
if (DriverSettings != null)
{
DriverSettings.ForEach(s => s.ValueChanged = false);
}
}
if (DriverSettings != null)
{
if (Preset != null)
{
foreach (var setting in DriverSettings)
{
setting.Value = setting.DefaultValue;
setting.ValueChanged = false;
}
foreach (var presetSetting in Preset.driverSettings)
{
var setting = DriverSettings.FirstOrDefault(s => s.SettingId == presetSetting.Key);
if (setting == null)
{
continue;
}
setting.Value = presetSetting.Value;
//setting.ValueChanged = false;
}
}
Groups = DriverSettings.OrderBy(s => s.GroupName).GroupBy(s => s.GroupName).Select(g =>
{
var settings = g.ToList();
var changedSettings = settings.Any(s => s.Value != s.DefaultValue && s.Value != NvSettingConstants.UnsetDwordValue);
var group = new GroupedSettings(g.Key, g.ToList(), changedSettings);
return group;
}).ToList();
}
StateHasChanged();
}
private async Task ApplyClick(MouseEventArgs e)
{
if (DriverSettings == null)
{
return;
}
var changedSettings = DriverSettings.Where(s => s.ValueChanged);
if (Preset != null)
{
Preset.UpdateDriverSettings(changedSettings);
return;
}
var changedSettingsDictionary = changedSettings.ToDictionary(k => k.SettingId, v => v.Value);
var result = await _rpcClientService.CallAsync<bool>("NvService", "SetDriverSettings", changedSettingsDictionary, CurrentProfileName);
}
private async Task ProfileOnChange()
{
if (CurrentProfileName?.Contains("Base") == true)
{
CurrentProfileName = null;
}
if (Preset != null)
{
Preset.DriverProfileName = CurrentProfileName;
}
DriverSettings = null;
await LoadDriverSettings();
}
private async Task OnFocus(FocusEventArgs e)
{
if (!IsVisible)
{
IsVisible = true;
ProfileNames ??= await _rpcClientService.CallAsync<List<string>>("NvService", "GetDriverProfileNames");
await LoadDriverSettings();
}
}
}
@@ -0,0 +1,618 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Services;
@using ColorControl.UI.Components.Pages.Generic
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common;
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@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-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>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="() => IsVisible = false"></button>
</div>
<div class="modal-body">
<form class="row g-1 needs-validation @WasValidated" novalidate>
@if (Preset == null)
{
<div>Loading...</div>
}
else
{
@if (!Preset.IsDisplayPreset)
{
<div class="mb-2">
<label class="form-label" for="name">Name</label>
<input class="form-control" type="text" @bind="Preset.name" required />
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="Preset.ShowInQuickAccess" id="showInQuickAccess">
<label class="form-check-label" for="showInQuickAccess">
Show in Quick Access
</label>
</div>
<ShortcutInput Label="Shortcut" @bind-Shortcut="Preset.shortcut" />
}
<div class="accordion accordion-flush" id="presetAccordion">
@if (!Preset.IsDisplayPreset)
{
<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; visibility: hidden" type="checkbox">
}
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#displaySettings">
Display
</button>
</span>
</h2>
<div id="displaySettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.primaryDisplay" id="primaryDisplay">
<label class="form-check-label" for="primaryDisplay">
Primary display
</label>
</div>
@if (displayPresets != null)
{
<div class="mb-2">
<label class="form-label" for="mode">Display</label>
<select class="form-select" id="mode" @bind="Preset.DisplayId" disabled="@(Preset.primaryDisplay)">
@foreach (var display in displayPresets)
{
<option value="@display.DisplayId">@display.displayName</option>
}
</select>
</div>
<div class="mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.DisplayConfig.IsPrimary" id="setAsPrimaryDisplay" disabled="@(Preset.primaryDisplay)">
<label class="form-check-label" for="setAsPrimaryDisplay">
Set display as primary display
</label>
</div>
}
</div>
</div>
</div>
}
@if (resolutions != null)
{
<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.DisplayConfig.ApplyResolution" id="applyResolution">
}
<button class="accordion-button py-2 @(!Preset.IsDisplayPreset ? "collapsed" : "") @(Preset.DisplayConfig.ApplyResolution ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#resolutionSettings">
Resolution
</button>
</span>
</h2>
<div id="resolutionSettings" class="accordion-collapse collapse @(Preset.IsDisplayPreset ? "show" : "")" data-bs-parent="#presetAccordion">
<div class="accordion-body">
@if (resolutions != null)
{
<div class="mb-2">
<label class="form-label" for="activeResolution">Active resolution</label>
<select class="form-select" id="activeResolution" @bind="selectedActiveResolution">
<option value="0x0">Not set</option>
@foreach (var resolution in resolutions)
{
<option value="@resolution.GetActiveString()">@resolution.GetActiveString()</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-label" for="virtualResolution">Virtual resolution</label>
<select class="form-select" id="virtualResolution" @bind="selectedVirtualResolution">
<option value="0x0">Not set</option>
@foreach (var resolution in resolutions)
{
<option value="@resolution.GetVirtualString()">@resolution.GetVirtualString()</option>
}
</select>
</div>
}
<div class="mb-2">
<label class="form-label" for="scaling">Scaling/Aspect ratio</label>
<select class="form-select" id="scaling" @bind="Preset.DisplayConfig.Scaling">
@foreach (var scaling in Enum.GetValues<Shared.Native.CCD.DisplayConfigScaling>().Where(s => s != Shared.Native.CCD.DisplayConfigScaling.ForceUint32))
{
<option value="@scaling.ToString()">@scaling.ToString()</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-label" for="rotation">Rotation</label>
<select class="form-select" id="rotation" @bind="Preset.DisplayConfig.Rotation">
@foreach (var rotation in Enum.GetValues<Shared.Native.CCD.DisplayConfigRotation>().Where(s => s != Shared.Native.CCD.DisplayConfigRotation.ForceUint32))
{
<option value="@rotation.ToString()">@rotation.ToString()</option>
}
</select>
</div>
</div>
</div>
</div>
}
@if (refreshRates != null)
{
<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.DisplayConfig.ApplyRefreshRate" id="applyRefreshRate">
}
<button class="accordion-button collapsed py-2 @(Preset.DisplayConfig.ApplyRefreshRate ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#refreshRateSettings">
Refresh Rate
</button>
</span>
</h2>
<div id="refreshRateSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
@if (refreshRates != null)
{
<div class="mb-2">
<label class="form-label" for="refreshRate">Refresh Rate</label>
<select class="form-select" id="refreshRate" @bind="selectedRefreshRate">
@foreach (var refreshRate in refreshRates)
{
<option value="@refreshRate.ToString()">@refreshRate.ToString() Hz</option>
}
</select>
</div>
}
</div>
</div>
</div>
}
<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.applyColorData" id="applyColorData">
}
<button class="accordion-button collapsed py-2 @(Preset.applyColorData ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#colorSettings">
Color settings
</button>
</span>
</h2>
<div id="colorSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.colorData.UseDefaultSettings" id="useDefaultSettings">
<label class="form-check-label" for="useDefaultSettings">
Use default settings
</label>
</div>
<div class="mb-2">
<label class="form-label" for="bitDepth">Bit depth</label>
<select class="form-select" id="bitDepth" @bind="Preset.colorData.ColorDepth" disabled="@Preset.colorData.UseDefaultSettings">
@foreach (var bitDepth in Enum.GetValues<ColorDataDepth>())
{
<option value="@bitDepth.ToString()">@bitDepth.ToString()</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-label" for="colorFormat">Format</label>
<select class="form-select" id="colorFormat" @bind="Preset.colorData.ColorFormat" disabled="@Preset.colorData.UseDefaultSettings">
@foreach (var format in Enum.GetValues<ColorDataFormat>())
{
<option value="@format.ToString()">@format.ToString()</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-label" for="dynamicRange">Dynamic range</label>
<select class="form-select" id="dynamicRange" @bind="Preset.colorData.DynamicRange" disabled="@Preset.colorData.UseDefaultSettings">
@foreach (var range in Enum.GetValues<ColorDataDynamicRange>())
{
<option value="@range.ToString()">@range.ToString()</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-label" for="colorimetry">Color space</label>
<select class="form-select" id="colorimetry" @bind="Preset.colorData.Colorimetry" disabled="@Preset.colorData.UseDefaultSettings">
@foreach (var space in Enum.GetValues<ColorDataColorimetry>())
{
<option value="@space.ToString()">@space.ToString()</option>
}
</select>
</div>
</div>
</div>
</div>
<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.ApplyColorEnhancements" id="ApplyColorEnhancements">
}
<button class="accordion-button collapsed py-2 @(Preset.ApplyColorEnhancements ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#colorEnhancements">
Color enhancements
</button>
</span>
</h2>
<div id="colorEnhancements" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<label class="form-label" for="digitalVibrance">Digital vibrance: @Preset.ColorEnhancementSettings.DigitalVibranceLevel</label>
<input class="form-range" type="range" min="0" max="100" @bind="Preset.ColorEnhancementSettings.DigitalVibranceLevel" id="digitalVibrance">
</div>
<div class="mb-2">
<label class="form-label" for="hue">Hue: @Preset.ColorEnhancementSettings.HueAngle°</label>
<input class="form-range" type="range" min="0" max="359" @bind="Preset.ColorEnhancementSettings.HueAngle" id="hue">
</div>
</div>
</div>
</div>
<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.applyDriverSettings" id="applyDriverSettings">
}
<button class="accordion-button collapsed py-2 @(Preset.applyDriverSettings ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#driverSettings">
Driver settings
</button>
</span>
</h2>
<div id="driverSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-3">
<button class="btn btn-secondary btn-sm" type="button" data-bs-target="#driverSettingsModal" data-bs-toggle="modal" @onclick="DriverSettingsOnClick">Open driver settings</button>
<span>@Preset.driverSettings.Count settings configured</span>
</div>
</div>
</div>
</div>
<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.ApplyNovideoSettings" id="applyNovideo">
}
<button class="accordion-button collapsed py-2 @(Preset.ApplyNovideoSettings ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#novideoSettings">
Novideo settings
</button>
</span>
</h2>
<div id="novideoSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.NovideoSettings.ApplyClamp" id="applyClamp">
<label class="form-check-label" for="applyClamp">
Apply clamp
</label>
</div>
<div class="mb-2">
<label class="form-label" for="novideoColorSpace">Target color space</label>
<select class="form-select" id="novideoColorSpace" @bind="Preset.NovideoSettings.ColorSpace">
@foreach (var colorSpace in Enum.GetValues<NovideoColorSpace>())
{
<option value="@colorSpace.ToString()">@colorSpace.ToString()</option>
}
</select>
</div>
</div>
</div>
</div>
<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.applyDithering" id="applyDithering">
}
<button class="accordion-button collapsed py-2 @(Preset.applyDithering ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#ditherSettings">
Dithering
</button>
</span>
</h2>
<div id="ditherSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<label class="form-label" for="state">State</label>
<select class="form-select" id="state" @bind="Preset.NvDitherState">
@foreach (var state in Enum.GetValues<NvDitherState>())
{
<option value="@state.ToString()">@state.GetDescription()</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-label" for="mode">Mode</label>
<select class="form-select" id="mode" @bind="Preset.NvDitherMode">
@foreach (var mode in Enum.GetValues<NvDitherMode>())
{
<option value="@mode.ToString()">@mode.GetDescription()</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-label" for="bits">Bit depth</label>
<select class="form-select" id="bits" @bind="Preset.NvDitherBits">
@foreach (var bits in Enum.GetValues<NvDitherBits>())
{
<option value="@bits.ToString()">@bits.GetDescription()</option>
}
</select>
</div>
</div>
</div>
</div>
<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.applyHDR" id="applyHDR">
}
<button class="accordion-button collapsed py-2 @(Preset.applyHDR ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#hdrSettings">
HDR
</button>
</span>
</h2>
<div id="hdrSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.HDREnabled" id="hdrEnabled">
<label class="form-check-label" for="hdrEnabled">
HDR enabled
</label>
</div>
@if (!Preset.IsDisplayPreset) {
<div class="mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.toggleHDR" id="toggleHdr">
<label class="form-check-label" for="toggleHdr">
Toggle HDR
</label>
</div>
}
<div class="mb-2">
<label class="form-label" for="mode">Output mode</label>
<select class="form-select" id="mode" @bind="Preset.HdrSettings.DisplayOutputMode">
@foreach (var mode in Enum.GetValues<NvOutputMode>())
{
<option value="@mode.ToString()">@mode.GetDescription()</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-range" for="sdrBrightness">SDR brightness (@Preset.SDRBrightness%)</label>
<input class="form-range" type="range" min="0" max="100" @bind="Preset.SDRBrightness" id="sdrBrightness">
</div>
</div>
</div>
</div>
@if (!Preset.IsDisplayPreset || Preset.MonitorConnectionType == NvAPIWrapper.Native.GPU.MonitorConnectionType.HDMI)
{
<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.applyHdmiSettings" id="applyHdmiSettings">
}
<button class="accordion-button collapsed py-2 @(Preset.applyHdmiSettings ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#hdmiSettings">
HDMI settings
</button>
</span>
</h2>
<div id="hdmiSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<label class="form-label" for="contentType">Content type</label>
<select class="form-select" id="contentType" @bind="Preset.HdmiInfoFrameSettings.ContentType" @oninput="HdmiSettingsOnInput">
@foreach (var type in Enum.GetValues<InfoFrameVideoContentType>())
{
<option value="@type.ToString()">@type.GetDescription()</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-label" for="colorimetry">Colorimetry</label>
<select class="form-select" id="colorimetry" @bind="Preset.HdmiInfoFrameSettings.Colorimetry" @oninput="HdmiSettingsOnInput">
@foreach (var colorimetry in Enum.GetValues<InfoFrameVideoColorimetry>())
{
<option value="@colorimetry.ToString()">@colorimetry.GetDescription()</option>
}
</select>
</div>
<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">
@foreach (var extendedColorimetry in Enum.GetValues<InfoFrameVideoExtendedColorimetry>())
{
<option value="@extendedColorimetry.ToString()">@extendedColorimetry.GetDescription()</option>
}
</select>
</div>
</div>
</div>
</div>
}
</div>
}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="() => IsVisible = false">Close</button>
<button type="submit" class="btn btn-primary" data-bs-dismiss="@(IsValid() ? "modal" : "")" @onclick="ApplyClick">@(Preset?.IsDisplayPreset == true ? "Apply" : "Save")</button>
</div>
</div>
</div>
</div>
@code {
public NvPreset? Preset { get; set; }
[Parameter]
public NvPreset? PresetParam { get; set; }
[Parameter]
public Func<NvPreset, Task>? AfterApply { get; set; }
[Parameter]
public Func<NvPreset, Task>? SetDriverSettingsPreset { get; set; }
private string? selectedActiveResolution;
private string? selectedVirtualResolution;
private string? selectedRefreshRate;
private List<VirtualResolution>? resolutions;
private List<Rational>? refreshRates;
private List<NvPreset>? displayPresets;
private string WasValidated = "";
private bool IsVisible;
private bool IsDriverSettingsOpened;
protected override async Task OnParametersSetAsync()
{
if (IsDriverSettingsOpened && Preset != null)
{
return;
}
Preset = PresetParam == null ? null : PresetParam.CloneWithId();
if (Preset != null && PresetParam != null)
{
resolutions = await _rpcClientService.CallAsync<List<VirtualResolution>>("NvService", "GetAvailableResolutionsV2", Preset);
refreshRates = await _rpcClientService.CallAsync<List<Rational>>("NvService", "GetAvailableRefreshRatesV2", Preset);
}
selectedActiveResolution = Preset?.DisplayConfig.Resolution?.GetActiveString();
selectedVirtualResolution = Preset?.DisplayConfig.Resolution?.GetVirtualString();
selectedRefreshRate = Preset?.DisplayConfig.RefreshRate?.ToString();
if (Preset?.IsDisplayPreset != true)
{
displayPresets = await _rpcClientService.CallAsync<List<NvPreset>>("NvService", "GetDisplayPresets");
}
}
protected override void OnAfterRender(bool firstRender)
{
// if (IsVisible && Preset?.IsDisplayPreset == false)
// {
// Preset.UpdateAutoApplySettings(keepChanges: true);
// }
}
private async Task ApplyClick(MouseEventArgs e)
{
if (Preset == null)
{
return;
}
if (!IsValid())
{
WasValidated = "was-validated";
return;
}
var newResolution = resolutions?.FirstOrDefault(r => r.GetActiveString() == selectedActiveResolution);
if (newResolution != null)
{
if (!string.IsNullOrEmpty(selectedVirtualResolution) && selectedActiveResolution != selectedVirtualResolution)
{
newResolution = new VirtualResolution(newResolution.ActiveWidth, newResolution.ActiveHeight);
var parts = selectedVirtualResolution.Split('x');
newResolution.VirtualWidth = uint.Parse(parts[0]);
newResolution.VirtualHeight = uint.Parse(parts[1]);
}
Preset.DisplayConfig.Resolution = newResolution;
}
else
{
Preset.DisplayConfig.Resolution = new VirtualResolution(0, 0);
}
var newRefreshRate = refreshRates?.FirstOrDefault(r => r.ToString() == selectedRefreshRate);
if (newRefreshRate != null)
{
Preset.DisplayConfig.RefreshRate = newRefreshRate;
}
Preset.ApplyNovideoSettings = Preset.NovideoSettings.ApplyClamp;
if (Preset.IsDisplayPreset)
{
Preset.UpdateAutoApplySettings(PresetParam);
await ApplyDisplayPreset();
}
else
{
var result = await _rpcClientService.CallAsync<bool>("NvService", "UpdatePreset", Preset);
}
if (AfterApply != null)
{
await AfterApply.Invoke(Preset);
}
}
private async Task ApplyDisplayPreset()
{
var result = await _rpcClientService.CallAsync<bool>("NvService", "ApplyPreset", Preset);
}
private void HdmiSettingsOnInput(ChangeEventArgs e)
{
Preset!.applyHdmiSettings = true;
}
private async Task DriverSettingsOnClick(MouseEventArgs e)
{
IsDriverSettingsOpened = true;
SetDriverSettingsPreset?.Invoke(Preset!);
}
private void ModalOnFocus(FocusEventArgs e)
{
if (IsVisible && IsDriverSettingsOpened)
{
IsDriverSettingsOpened = false;
StateHasChanged();
}
if (!IsVisible)
{
WasValidated = "";
IsVisible = true;
}
}
private bool IsValid()
{
return Preset?.IsDisplayPreset == true || !string.IsNullOrEmpty(Preset?.name);
}
}
@@ -0,0 +1,76 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
<div class="modal" id="settingsModal" tabindex="-1" @onfocus="OnFocus">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">NVIDIA settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="() => Config = null"></button>
</div>
<div class="modal-body">
<form class="row g-1">
@if (Config == null)
{
<div>Loading...</div>
}
else
{
<ShortcutInput Label="Quick Access Shortcut" @bind-Shortcut="Config.QuickAccessShortcut" />
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="Config.ShowOverclocking" id="showOverclocking">
<label class="form-check-label" for="showOverclocking">
Show overclocking
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="Config.ApplyNovideoOnStartup" id="applyNovideoOnStartup">
<label class="form-check-label" for="applyNovideoOnStartup">
Apply Novideo settings on startup
</label>
</div>
}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="() => Config = null">Close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" @onclick="ApplyClick">Save</button>
</div>
</div>
</div>
</div>
@code {
private NvServiceConfig? Config { get; set; }
private async Task ApplyClick(MouseEventArgs e)
{
if (Config == null)
{
return;
}
var result = await _rpcClientService.CallAsync<bool>("NvService", "UpdateConfig", Config);
if (result)
{
Config = null;
}
}
private async Task OnFocus(FocusEventArgs e)
{
if (Config != null)
{
return;
}
Config = await _rpcClientService.CallAsync<NvServiceConfig>("NvService", "GetConfig");
}
}
@@ -0,0 +1,251 @@
@page "/NVIDIA"
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.Shared.Common;
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject ProtectedLocalStorage _localStorage
<PageTitle>NVIDIA controller</PageTitle>
<NvidiaSettings />
<NvidiaDithering Visible="DitheringVisible" />
<NvidiaDriverSettings Preset="CurrentDriverSettingsPreset" />
<div class="d-flex align-items-center">
<h1>NVIDIA controller</h1>
<div class="d-flex ms-auto">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle me-2" data-bs-toggle="dropdown" aria-expanded="false">
Tools
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#ditheringModal" @onclick="() => DitheringVisible = true">Test dithering</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#driverSettingsModal" @onclick="() => CurrentDriverSettingsPreset = null">Driver settings</a></li>
</ul>
</div>
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#settingsModal">
Settings
</button>
</div>
</div>
<h2>Displays</h2>
<NvidiaPreset PresetParam="CurrentPreset" AfterApply="AfterPresetApplied" SetDriverSettingsPreset="SetDriverSettingsPreset" />
<ConfirmModal Title="Delete preset" Message="Are you sure you want to delete the preset?" OnConfirm="PresetOnDeleteConfirmed" />
@if (DisplayPresets == null)
{
<div>Loading...</div>
}
else
{
<div class="row mb-3">
<div class="col">
@if (!DisplayPresets.Any())
{
<span>No displays found</span>
}
@foreach (var preset in DisplayPresets)
{
<div class="card mb-3">
<div class="card-header">
@preset.displayName
@if (preset.DisplayConfig.IsPrimary == true)
{
<span class="ms-2 badge text-bg-primary" title="Primary display">Primary</span>
}
<button type="button" class="btn btn-sm btn-primary ms-2" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => DisplayPresetOnClick(preset)">
Settings
</button>
</div>
<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>
</div>
</div>
}
</div>
</div>
}
@if (Presets == null)
{
<div>Loading...</div>
}
else
{
<div class="mb-2 d-flex align-items-center">
<h2>Presets</h2>
<div class="d-flex ms-auto">
<select class="form-select me-2" id="presetOrder" @bind="PresetOrder" @bind:after="PresetOrderChanged">
@foreach (var order in Enum.GetValues<PresetOrder>())
{
<option value="@order">Order by: @order.GetDescription()</option>
}
</select>
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => AddPresetOnClick()">
Add
</button>
</div>
</div>
<div class="row mb-3">
<div class="col">
<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>
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="mb-1">
@preset.name <small class="text-muted">@(preset.primaryDisplay ? "Primary display" : preset.displayName)</small>
@if (preset.ShowInQuickAccess)
{
<span class="ms-2 badge text-bg-warning" title="Show in Quick Access">QA</span>
}
</h5>
</div>
<span>
<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>
<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 class="text-muted">@preset.shortcut</small>
</a>
}
</div>
</div>
</div>
}
@code {
private List<NvPreset>? Presets;
private List<NvPreset>? DisplayPresets;
private NvPreset? CurrentPreset;
private NvPreset? CurrentDriverSettingsPreset;
private bool DitheringVisible;
private PresetOrder PresetOrder;
protected override async Task OnInitializedAsync()
{
await RefreshPresets();
DisplayPresets = await _rpcClientService.CallAsync<List<NvPreset>>("NvService", "GetDisplayPresets");
}
protected override async Task OnAfterRenderAsync(bool firstTime)
{
if (firstTime)
{
PresetOrder = await _localStorage.TryGet<PresetOrder>("NvidiaPresetOrder");
Presets = Presets?.OrderPresetsBy(PresetOrder);
StateHasChanged();
}
}
private async Task PresetOnClick(NvPreset preset)
{
await _rpcClientService.CallAsync<bool>("NvService", "ApplyPresetWithId", preset.id);
}
private async Task PresetOnEditClick(NvPreset preset)
{
CurrentPreset = preset;
}
private async Task PresetOnCloneClick(NvPreset preset)
{
CurrentPreset = new NvPreset(preset);
CurrentPreset.SetCloneValues();
}
private async Task PresetOnDeleteClick(NvPreset preset)
{
CurrentPreset = preset;
}
private async Task PresetOnDeleteConfirmed()
{
if (CurrentPreset == null)
{
return;
}
await _rpcClientService.CallAsync<bool>("NvService", "DeletePreset", CurrentPreset.id);
await RefreshPresets();
StateHasChanged();
}
private async Task DisplayPresetOnClick(NvPreset preset)
{
CurrentPreset = preset;
}
private async Task AddPresetOnClick()
{
CurrentPreset = new NvPreset();
}
private async Task AfterPresetApplied(NvPreset preset)
{
if (preset.IsDisplayPreset)
{
DisplayPresets = await _rpcClientService.CallAsync<List<NvPreset>>("NvService", "GetDisplayPresets");
}
else
{
await RefreshPresets();
}
StateHasChanged();
}
private async Task SetDriverSettingsPreset(NvPreset preset)
{
CurrentDriverSettingsPreset = preset;
StateHasChanged();
}
private async Task PresetOrderChanged()
{
await _localStorage.SetAsync("NvidiaPresetOrder", PresetOrder);
await RefreshPresets();
}
private async Task RefreshPresets()
{
Presets = await _rpcClientService.CallAsync<List<NvPreset>>("NvService", "GetPresets");
Presets = Presets?.OrderPresetsBy(PresetOrder);
}
}
+300 -32
View File
@@ -1,52 +1,320 @@
@page "/options"
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode InteractiveServer
@inject OptionsService _optionsService
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager;
<PageTitle>Options</PageTitle>
<h1>Options</h1>
<div class="card">
<div class="card-body">
<h5 class="card-title">General</h5>
@if (config == null)
{
<div>Loading...</div>
}
else
{
<div class="row">
<div class="col-lg-6 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">General</h5>
<form>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.AutoStart" id="startAfterLogin" @oninput="StartAfterLoginInput">
<label class="form-check-label" for="startAfterLogin">
Automatically start after login
</label>
</div>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.CheckForUpdates" id="checkForUpdates" @oninput="CheckForUpdatesInput">
<label class="form-check-label" for="checkForUpdates">
Automatically check for updates
</label>
</div>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.AutoInstallUpdates" id="autoInstallUpdates" @oninput="AutoInstallUpdatesInput">
<label class="form-check-label" for="autoInstallUpdates">
Automatically install updates
</label>
</div>
<div class="mb-3">
<label class="form-label" for="elevationMethod">
Elevation Method
</label>
<select class="form-select" @bind="elevationMethod" id="elevationMethod" @oninput="ElevationMethodInput" aria-describedby="elevationMethodHelp">
<option value="0">None</option>
<option value="1">Run as administrator</option>
<option value="2">Use service</option>
<option value="3">Use elevated process</option>
</select>
<div id="elevationMethodHelp" class="form-text">
Elevation is needed in some cases where ColorControl needs administrator rights.
Some operations like installing a service, changing the priority of a process or creating a temporary IP-route for improved WOL-functionality will not work without those rights.
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-6 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">UI-settings</h5>
<form>
<div class="mb-2">
<label class="form-label" for="uiType">Default UI</label>
<select class="form-select" id="uiType" @bind="config.UiType" @oninput="UiTypeOnInput">
@foreach (var type in Enum.GetValues<UiType>())
{
<option value="@type.ToString()">@type.GetDescription()</option>
}
</select>
</div>
<div class="mb-3">
<label class="form-label" for="webUiPort">
Web UI port
</label>
<input class="form-control" type="number" @bind="config.UiPort" id="webUiPort" @bind:after="UiPortBlur">
</div>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.MinimizeOnClose" id="minimizeOnClose" @oninput="MinimizeOnCloseInput">
<label class="form-check-label" for="minimizeOnClose">
Minimize on close
</label>
</div>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.StartMinimized" id="startMinimized" @oninput="StartMinimizedInput">
<label class="form-check-label" for="startMinimized">
Start minimized
</label>
</div>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.MinimizeToTray" id="minimizeToTray" @oninput="MinimizeToTrayInput">
<label class="form-check-label" for="minimizeToTray">
Minimize to system tray
</label>
</div>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.UseDarkMode" id="useDarkMode" @oninput="UseDarkModeInput">
<label class="form-check-label" for="useDarkMode">
Dark Mode
</label>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-6 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Modules</h5>
<form>
@foreach (var module in config.Modules)
{
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="@("module_" + module.DisplayName)" @bind="module.IsActive" @oninput="(ChangeEventArgs e) => ModuleOnInput(module, e)">
<label class="form-check-label" for="@("module_" + module.DisplayName)">@module.DisplayName</label>
</div>
}
</form>
</div>
</div>
</div>
<div class="col-lg-6 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Advanced</h5>
<form>
<div class="mb-3 form-check">
<label class="form-range" for="processMonitorPollingInterval">
Polling interval of process monitor (@config.ProcessMonitorPollingInterval milliseconds)
</label>
<input class="form-range" type="range" min="50" max="10000" step="50" @bind="config.ProcessMonitorPollingInterval" id="processMonitorPollingInterval" aria-describedby="processMonitorPollingIntervalHelp" @bind:after="PollingIntervalOnInput">
<div id="processMonitorPollingIntervalHelp" class="form-text">Decreasing this delay may execute triggered presets sooner but can cause a higher CPU load</div>
</div>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.UseRawInput" id="useRawInput" aria-describedby="useRawInputHelp" @oninput="UseRawInputInput">
<label class="form-check-label" for="useRawInput">
Use Raw Input for shortcuts (hot keys)
</label>
<div id="useRawInputHelp" class="form-text">This enables shortcuts to work during applications/games that block certain keys (like WinKey or Control). NOTE: if the application in the foreground runs with higher privileges than ColorControl, Raw Input does not work and normal hot keys are used</div>
</div>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.SetMinTmlAndMaxTml" id="setMinTmlAndMaxTml" @oninput="SetMinMaxTmlInput">
<label class="form-check-label" for="setMinTmlAndMaxTml">
Set MinTML and MaxTML when applying color profiles
</label>
</div>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.DisableErrorPopupWhenApplyingPreset" id="disableErrorPopupWhenApplyingPreset" @oninput="DisableErrorPopupInput">
<label class="form-check-label" for="disableErrorPopupWhenApplyingPreset">
Disable error popup when applying presets
</label>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-6 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Miscellaneous</h5>
<form>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.FixChromeFonts" id="fixChromeFonts" aria-describedby="fixChromeFontsHelp" @bind:after="FixChromeFontsInput">
<label class="form-check-label" for="fixChromeFonts">
ClearType: fix the bad fonts in Chrome when using a monitor with a different pixel structure
</label>
<div id="fixChromeFontsHelp" class="form-text">This will add the parameter --disable-lcd-text to Chrome which enables grayscale anti-aliasing</div>
</div>
<ShortcutInput Label="Shortcut to start the blank screensaver" @bind-Shortcut="config.ScreenSaverShortcut" @bind-Shortcut:after="UpdateScreenSaverShortcut" />
</form>
</div>
</div>
</div>
</div>
<form class="ms-3">
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.AutoStart" id="startAfterLogin">
<label class="form-check-label" for="startAfterLogin">
Automatically start after login
</label>
</div>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.StartMinimized" id="startMinimized">
<label class="form-check-label" for="startMinimized">
Start minimized
</label>
</div>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.MinimizeToTray" id="minimizeToTray">
<label class="form-check-label" for="minimizeToTray">
Minimize to tray
</label>
</div>
<div class="mb-3">
<button class="btn btn-primary" @onclick="SaveConfig">Save</button>
</div>
</form>
</div>
}
@code {
private Config config;
private Config? config;
private string elevationMethod = "0";
protected override async Task OnInitializedAsync()
{
config = _optionsService.GetConfig();
config = await _rpcClientService.CallAsync<Config>("OptionsService", "GetConfig");
elevationMethod = ((int)config.ElevationMethod).ToString();
}
private void SaveConfig()
private async Task SaveConfig()
{
_optionsService.SetConfig(config);
await _rpcClientService.CallAsync<bool>("OptionsService", "SetConfig", config);
}
private async Task UseDarkModeInput(ChangeEventArgs e)
{
var value = e.Value != null ? (bool)e.Value : false;
var result = await UpdateBoolSetting("SetDarkMode", e);
if (result)
{
AppState.SetTheme(value ? "dark" : "light");
//ThemeTransitionName = AppState.SelectedTheme == "light" ? "Dark theme" : "Light theme";
_navigationManager.Refresh();
}
}
private async Task StartAfterLoginInput(ChangeEventArgs e)
{
await UpdateBoolSetting("SetStartAfterLogin", e);
}
private async Task MinimizeOnCloseInput(ChangeEventArgs e)
{
await UpdateBoolSetting("SetMinimizeOnClose", e);
}
private async Task StartMinimizedInput(ChangeEventArgs e)
{
await UpdateBoolSetting("SetStartMinimized", e);
}
private async Task MinimizeToTrayInput(ChangeEventArgs e)
{
await UpdateBoolSetting("SetMinimizeToTray", e);
}
private async Task CheckForUpdatesInput(ChangeEventArgs e)
{
await UpdateBoolSetting("SetCheckForUpdates", e);
}
private async Task AutoInstallUpdatesInput(ChangeEventArgs e)
{
await UpdateBoolSetting("SetAutoInstallUpdates", e);
}
private async Task ModuleOnInput(Module module, ChangeEventArgs e)
{
module.IsActive = e.Value != null ? (bool)e.Value : false;
var result = await _rpcClientService.CallAsync<bool>("OptionsService", "UpdateModule", module);
if (result)
{
_navigationManager.NavigateTo("/options", true);
}
}
private async Task PollingIntervalOnInput()
{
await _rpcClientService.CallAsync<bool>("OptionsService", "SetProcessMonitorPollingInterval", config!.ProcessMonitorPollingInterval);
}
private async Task UseRawInputInput(ChangeEventArgs e)
{
await UpdateBoolSetting("SetUseRawInput", e);
}
private async Task SetMinMaxTmlInput(ChangeEventArgs e)
{
await UpdateBoolSetting("SetMinTmlAndMaxTml", e);
}
private async Task DisableErrorPopupInput(ChangeEventArgs e)
{
await UpdateBoolSetting("SetDisableErrorPopupWhenApplyingPreset", e);
}
private async Task FixChromeFontsInput()
{
await UpdateBoolSetting2("SetFixChromeFonts", config!.FixChromeFonts);
}
private async Task UpdateScreenSaverShortcut()
{
await _rpcClientService.CallAsync<bool>("OptionsService", "SetScreenSaverShortcut", config!.ScreenSaverShortcut);
}
private async Task<bool> UpdateBoolSetting(string method, ChangeEventArgs e)
{
var value = e.Value != null ? (bool)e.Value : false;
return await UpdateBoolSetting2(method, value);
}
private async Task<bool> UpdateBoolSetting2(string method, bool value)
{
return await _rpcClientService.CallAsync<bool>("OptionsService", method, value);
}
private async Task ElevationMethodInput(ChangeEventArgs e)
{
var value = e.Value != null ? (ElevationMethod)int.Parse((string)e.Value) : ElevationMethod.None;
await _rpcClientService.CallAsync<bool>("OptionsService", "SetElevationMethod", value);
config!.ElevationMethod = value;
}
private async Task UiTypeOnInput(ChangeEventArgs e)
{
if (Enum.TryParse<UiType>(e.Value?.ToString(), out var uiType))
{
await _rpcClientService.CallAsync<bool>("OptionsService", "SetUiType", uiType);
config!.UiType = uiType;
}
}
private async Task UiPortBlur()
{
await _rpcClientService.CallAsync<bool>("OptionsService", "SetUiPort", config!.UiPort);
}
}
@@ -0,0 +1,237 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.Shared.Services
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@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-content">
<div class="modal-header">
<h5 class="modal-title">@(Device?.Name == null ? $"Add new device" : $"Settings of device {Device.Name}")</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="() => IsVisible = false"></button>
</div>
<div class="modal-body">
<form class="row g-1 needs-validation @WasValidated" novalidate>
@if (Device == null)
{
<div>Loading...</div>
}
else
{
<div class="mb-2">
<label class="form-label" for="name">Name</label>
<input class="form-control" id="name" type="text" @bind="Device.Name" required />
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="Device.IsCustom" id="isCustom">
<label class="form-check-label" for="isCustom">
Is custom device
</label>
</div>
<div class="mb-2">
<label class="form-label" for="ipAddress">IP-address</label>
<input class="form-control" id="ipAddress" type="text" @bind="Device.IpAddress" disabled="@(!Device.IsCustom)" required="Device.IsCustom" />
</div>
<div class="mb-2">
<label class="form-label" for="macAddress">MAC-address</label>
<input class="form-control" id="macAddress" type="text" @bind="Device.MacAddress" disabled="@(!Device.IsCustom)" />
</div>
@if (!Device.IsConnected)
{
<div class="mb-2">
<button class="btn btn-primary me-2" type="button" disabled="@IsTestingConnection" @onclick="TestConnectionClick">
@if (IsTestingConnection)
{
<span class="spinner-border spinner-border-sm me-1" aria-hidden="true"></span>
}
<span role="status">Test connection</span>
</button>
@if (IsTestConnectionSuccessful != null)
{
if (IsTestConnectionSuccessful == true)
{
<span class="text-success">Connection test was successful!</span>
}
else
{
<span class="text-danger">Connection failed!</span>
}
}
</div>
}
<div class="accordion accordion-flush" id="accordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#deviceOptions">
Device options
</button>
</h2>
<div id="deviceOptions" class="accordion-collapse collapse" data-bs-parent="#accordion">
<div class="accordion-body">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOnAfterStartup" @bind="Device.Options.PowerOnAfterStartup">
<label class="form-check-label" for="checkPowerOnAfterStartup">Power on after startup</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOnAfterResume" @bind="Device.Options.PowerOnAfterResume">
<label class="form-check-label" for="checkPowerOnAfterResume">Power on after resume from standby</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOffOnShutdown" @bind="Device.Options.PowerOffOnShutdown">
<label class="form-check-label" for="checkPowerOffOnShutdown">Power off on shutdown</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOffOnStandby" @bind="Device.Options.PowerOffOnStandby">
<label class="form-check-label" for="checkPowerOffOnStandby">Power off on standby</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOffOnScreensaverStart" @bind="Device.Options.PowerOffOnScreenSaver">
<label class="form-check-label" for="checkPowerOffOnScreensaverStart">Power off when screensaver activates</label>
<button class="btn btn-sm btn-primary ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#screenSaverSettings" aria-expanded="false" aria-controls="screenSaverSettings">
Settings
</button>
</div>
<div class="collapse mb-3" id="screenSaverSettings">
<div class="card card-body">
<div class="mb-2">
<label class="form-label" for="minScreenSaverDuration">Minimal screen saver duration (seconds)</label>
<input class="form-control" id="minScreenSaverDuration" type="number" @bind="Device.Options.ScreenSaverMinimalDuration" />
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkTurnScreenOffInsteadOfPowerOff" @bind="Device.Options.TurnScreenOffOnScreenSaver">
<label class="form-check-label" for="checkTurnScreenOffInsteadOfPowerOff">Turn screen off instead of power off</label>
<div class="form-text">NOTE: turning the screen off might not work correctly on all models</div>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkTurnScreenOnInsteadOfPowerOn" @bind="Device.Options.TurnScreenOnAfterScreenSaver">
<label class="form-check-label" for="checkTurnScreenOnInsteadOfPowerOn">Turn screen on instead of power on</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPerformActionOnManualScreenSaver" @bind="Device.Options.HandleManualScreenSaver">
<label class="form-check-label" for="checkPerformActionOnManualScreenSaver">Perform action even on manually executed screen saver</label>
</div>
</div>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOnOnScreensaverEnd" @bind="Device.Options.PowerOnAfterScreenSaver">
<label class="form-check-label" for="checkPowerOnOnScreensaverEnd">Power on when screensaver deactivates</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOnAfterManualPowerOff" @bind="Device.Options.PowerOnAfterManualPowerOff">
<label class="form-check-label" for="checkPowerOnAfterManualPowerOff">Power on even after manual power off</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkAllowTriggers" @bind="Device.Options.TriggersEnabled">
<label class="form-check-label" for="checkAllowTriggers">Allow triggers to be fired for this device</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkWindowsPowerOn" @bind="Device.Options.PowerOnByWindows">
<label class="form-check-label" for="checkWindowsPowerOn">Use Windows power settings to power on</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkWindowsPowerOff" @bind="Device.Options.PowerOffByWindows">
<label class="form-check-label" for="checkWindowsPowerOff">Use Windows power settings to power off</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkUseSecureConnection" @bind="Device.Options.UseSecureConnection">
<label class="form-check-label" for="checkUseSecureConnection">Use secure connection</label>
</div>
</div>
</div>
</div>
</div>
}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="() => IsVisible = false">Close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="@(IsValid() ? "modal" : "")" @onclick="ApplyClick">Save</button>
</div>
</div>
</div>
</div>
@code {
private SamsungDeviceDto? _device;
[Parameter]
public SamsungDeviceDto? Device { get; set; }
[Parameter]
public Func<SamsungDeviceDto, Task>? AfterApply { get; set; }
private bool IsTestingConnection;
private bool? IsTestConnectionSuccessful;
private string WasValidated = "";
private bool IsVisible;
protected override async Task OnParametersSetAsync()
{
if (_device != Device)
{
_device = Device;
}
}
private async Task ApplyClick(MouseEventArgs e)
{
if (Device == null)
{
return;
}
if (!IsValid())
{
WasValidated = "was-validated";
return;
}
var result = await _rpcClientService.CallAsync<bool>("SamsungService", "UpdateDevice", Device);
if (AfterApply != null)
{
await AfterApply.Invoke(Device);
}
}
private async Task TestConnectionClick(MouseEventArgs e)
{
IsTestingConnection = true;
try
{
IsTestConnectionSuccessful = await _rpcClientService.CallWithOptionsAsync<bool>("SamsungService", "TestDevice", new RpcUiClientOptions { Timeout = 120000 }, Device);
}
finally
{
IsTestingConnection = false;
}
}
private void ModalOnFocus(FocusEventArgs e)
{
if (!IsVisible)
{
WasValidated = "";
IsVisible = true;
}
}
private bool IsValid()
{
return !string.IsNullOrEmpty(Device?.Name) && (Device?.IsCustom != true || !string.IsNullOrEmpty(Device?.IpAddress));
}
}
@@ -0,0 +1,173 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.Shared.Services
@using ColorControl.UI.Components.Pages.Generic
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common
@using static ColorControl.UI.Components.Pages.Generic.StepsInput<ColorControl.Shared.Contracts.Samsung.SamsungPreset>
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@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-content">
<div class="modal-header">
<h5 class="modal-title">@(Preset?.id == 0 ? $"Settings for new preset" : $"Settings of preset {Preset?.IdOrName}")</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="() => IsVisible = false"></button>
</div>
<div class="modal-body">
<form class="row g-1 needs-validation @WasValidated" novalidate>
@if (Preset == null)
{
<div>Loading...</div>
}
else
{
<div class="mb-2">
<label class="form-label" for="name">Name</label>
<input class="form-control" id="name" type="text" @bind="Preset.name" required />
</div>
@if (Devices != null)
{
<div class="mb-2">
<label class="form-label" for="device">Device</label>
<select class="form-select" id="device" @bind="Preset.DeviceMacAddress">
<option value="">Globally selected device</option>
@foreach (var device in Devices)
{
<option value="@device.MacAddress">@device.Name</option>
}
</select>
</div>
}
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" @bind="Preset.ShowInQuickAccess" id="showInQuickAccess">
<label class="form-check-label" for="showInQuickAccess">
Show in Quick Access
</label>
</div>
<ShortcutInput Label="Shortcut" @bind-Shortcut="Preset.shortcut" />
<div class="accordion accordion-flush" id="presetAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#advancedSettings">
Advanced settings
</button>
</h2>
<div id="advancedSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<TriggerInput Triggers="Preset.Triggers"></TriggerInput>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#steps">
Steps
</button>
</h2>
<div id="steps" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<StepsInput Label="Steps" @bind-Steps="Preset.Steps" CustomItems="CustomItems" ActionItems="CustomActions" SamsungPresetsEnabled="false" TPreset="SamsungPreset"></StepsInput>
<div class="mb-2">
<label class="form-label" for="description">Description</label>
<textarea class="form-control" id="description" type="text" @bind="Preset.Description" />
</div>
</div>
</div>
</div>
</div>
}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="() => IsVisible = false">Close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="@(IsValid() ? "modal" : "")" @onclick="ApplyClick">Save</button>
</div>
</div>
</div>
</div>
@code {
private SamsungPreset? Preset;
[Parameter]
public SamsungPreset? PresetParam { get; set; }
[Parameter]
public List<SamsungDeviceDto>? Devices { get; set; }
[Parameter]
public Func<SamsungPreset, Task>? AfterApply { get; set; }
private List<CustomStepItem>? CustomItems;
private List<ActionStepItem>? CustomActions;
private string WasValidated = "";
private bool IsVisible;
protected override async Task OnParametersSetAsync()
{
if (PresetParam == null)
{
return;
}
Preset = PresetParam.CloneWithId();
}
protected override async Task OnInitializedAsync()
{
CustomItems = [new CustomStepItem("RC buttons", Enum.GetValues<ButtonType>().Select(b => b.ToString()).ToList())];
var actions = await _rpcClientService.CallAsync<List<InvokableActionDto<SamsungPreset>>>("SamsungService", "GetInvokableActions");
if (actions != null)
{
CustomActions = [new ActionStepItem("Actions", actions.Where(a => !a.Advanced).ToList())];
}
}
private async Task ApplyClick(MouseEventArgs e)
{
if (Preset == null)
{
return;
}
if (!IsValid())
{
WasValidated = "was-validated";
return;
}
var result = await _rpcClientService.CallAsync<bool>("SamsungService", "UpdatePreset", Preset);
if (AfterApply != null)
{
await AfterApply.Invoke(Preset);
}
}
private void ModalOnFocus(FocusEventArgs e)
{
if (!IsVisible)
{
WasValidated = "";
IsVisible = true;
}
}
private bool IsValid()
{
return !string.IsNullOrEmpty(Preset?.name);
}
}
@@ -0,0 +1,233 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
<FieldsModal ModalId="serviceMenuModal" Title="@Title" ConfirmCaption="@ConfirmCaption" SecondaryCaption="@SecondaryCaption"
Fields="Fields" OnConfirm="NextClick" OnSecondary="SecondaryClick" Dismiss="@Dismiss" OnShow="OnShow"></FieldsModal>
@code {
[Parameter]
public List<SamsungDeviceDto>? Devices { get; set; }
private string? Title { get; set; }
private string? ConfirmCaption { get; set; }
private string? SecondaryCaption { get; set; }
private string? Dismiss { get; set; }
private List<FieldDefinition>? Fields { get; set; }
private FieldDefinition? DeviceField { get; set; }
private Func<Task>? NextAction;
private Func<Task>? SecondaryAction;
protected override async Task OnInitializedAsync()
{
// NextAction = FirstStepExit;
// Title = "Access Service Menu";
// ConfirmCaption = "Next >";
// Fields = [AddField("Service Menu Access Type", ServiceMenuType.FactoryMenu, subLabel: "Both types will not reset your settings. 'FactoryMenu' is recommended for 2024 series but will also work for older ones. Click 'Next >' to continue or 'X' to stop")];
}
protected async Task OnShow()
{
NextAction = FirstStepExit;
Title = "Access Service Menu";
ConfirmCaption = "Next >";
SecondaryCaption = null;
SecondaryAction = null;
Dismiss = null;
Fields = [AddField("Service Menu Access Type", ServiceMenuType.FactoryMenu, subLabel: "Both types will not reset your settings. 'FactoryMenu' is recommended for 2024 series but will also work for older ones. Click 'Next >' to continue or 'X' to stop")];
if (Devices?.Any() == true)
{
DeviceField = FieldDefinition.CreateDropDownField("Device", Devices.Select(d => d.ToString()).ToList());
DeviceField.Value = Devices.FirstOrDefault(d => d.IsSelected)?.ToString();
Fields.Insert(0, DeviceField);
}
StateHasChanged();
}
private async Task NextClick()
{
if (NextAction == null)
{
return;
}
await NextAction();
}
private async Task SecondaryClick()
{
if (SecondaryAction != null)
{
await SecondaryAction();
}
}
private async Task FirstStepExit()
{
if (Fields == null)
{
return;
}
var menuType = Fields.First(f => f.ValueType == typeof(ServiceMenuType)).ValueAsEnum<ServiceMenuType>();
if (menuType == ServiceMenuType.FactoryMenu)
{
await FactoryMenuFirstStepEnter();
}
else
{
await HospitalityMenuFirstStepEnter();
}
}
protected async Task FactoryMenuFirstStepEnter()
{
await ExecutePreset(SamsungFactoryMenuPresets.Preset1);
NextAction = FactoryMenuSecondStepEnter;
Title = "Access Service Menu - Step 2";
ConfirmCaption = "Next >";
SecondaryCaption = "Skip";
SecondaryAction = FactoryMenuSecondStepEnterWithoutExecute;
Fields = [AddField("Service Menu - Advanced", null, FieldType.Label, "If the service menu opened and you want to access the Advanced settings, click 'Next >' or click 'Skip' to skip")];
StateHasChanged();
}
protected async Task FactoryMenuSecondStepEnterWithoutExecute()
{
NextAction = FactoryMenuSecondStepExit;
Title = "Exit Service Menu";
ConfirmCaption = "Next >";
SecondaryCaption = null;
SecondaryAction = null;
Fields = [AddField("Exit Service Menu Type", ExitServiceMenuType.Exit, subLabel: "Choose how to exit the service menu. You can either reboot the TV or just exit the service menu (if, for example, no changes have been made)")];
StateHasChanged();
}
protected async Task FactoryMenuSecondStepEnter()
{
await ExecutePreset(SamsungFactoryMenuPresets.PresetAdvanced);
await FactoryMenuSecondStepEnterWithoutExecute();
}
protected async Task FactoryMenuSecondStepExit()
{
if (Fields == null)
{
return;
}
var menuType = Fields.First().ValueAsEnum<ExitServiceMenuType>();
if (menuType == ExitServiceMenuType.Exit)
{
await ExitServiceMenuEnter();
}
else
{
await RebootTvEnter();
}
}
protected async Task ExitServiceMenuEnter()
{
await ExecutePreset(SamsungFactoryMenuPresets.Exit);
NextAction = null;
Title = "Exited Service Menu";
Dismiss = "modal";
ConfirmCaption = "OK";
SecondaryCaption = "Factory";
SecondaryAction = ExecuteFactoryKey;
Fields = [AddField("Service Menu - Exited", null, FieldType.Label, "Exited the service menu. Click OK to close. If the TV is stuck on cycling full screen RGB images, use the 'Factory' button below.")];
StateHasChanged();
}
protected async Task RebootTvEnter()
{
await ExecutePreset(SamsungGenericPresets.Reboot);
NextAction = null;
Title = "Rebooted TV";
Dismiss = "modal";
ConfirmCaption = "OK";
Fields = [AddField("Service Menu - Rebooted TV", null, FieldType.Label, "Rebooted the TV. Power on the TV manually if the TV does not come on automatically.")];
StateHasChanged();
}
protected async Task HospitalityMenuFirstStepEnter()
{
await ExecutePreset(SamsungHospitalityMenuPresets.Preset1);
NextAction = HospitalityMenuSecondStepEnter;
Title = "Access Service Menu - Step 2";
ConfirmCaption = "Next >";
Fields = [AddField("Service Menu opened?", null, FieldType.Label, "If the menu is opened with the 'Hospitality Mode' item highlighted and the rest disabled, click 'Next >' to continue or 'X' to stop")];
StateHasChanged();
}
protected async Task HospitalityMenuSecondStepEnter()
{
await ExecutePreset(SamsungHospitalityMenuPresets.Preset2);
NextAction = HospitalityMenuThirdStepEnter;
Title = "Access Service Menu - Step 3";
ConfirmCaption = "Next >";
SecondaryCaption = "Skip";
Fields = [AddField("Service Menu - Advanced", null, FieldType.Label, "If the 'Advanced' item is highlighted and you want to access these settings, click 'Next >' or click 'Skip' to skip")];
StateHasChanged();
}
protected async Task HospitalityMenuThirdStepEnter()
{
await ExecutePreset(SamsungHospitalityMenuPresets.Preset3);
await FactoryMenuSecondStepEnterWithoutExecute();
}
protected async Task ExecuteFactoryKey()
{
await ExecutePreset(SamsungFactoryMenuPresets.KeyFactory);
}
private async Task ExecutePreset(SamsungPreset preset)
{
if (Devices != null && DeviceField?.Value != null)
{
var device = Devices.FirstOrDefault(d => d.ToString() == DeviceField.Value.ToString());
preset.DeviceMacAddress = device?.MacAddress;
}
await _rpcClientService.CallAsync<bool>("SamsungService", "ApplyPreset", preset);
//await Task.Delay(2000);
}
private FieldDefinition AddField(string label, object? value, FieldType fieldType = FieldType.DropDown, string? subLabel = null)
{
return new FieldDefinition
{
FieldType = fieldType,
Value = value,
StringValue = value?.ToString(),
ValueType = value?.GetType(),
Label = label,
SubLabel = subLabel
};
}
}
@@ -0,0 +1,34 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
<FieldsModal Title="Samsung settings" ConfirmCaption="Save" ModalId="settingsModal" Fields="Fields" OnConfirm="ApplyClick" OnShow="ModalOnShow"></FieldsModal>
@code {
private SamsungServiceConfigDto? Config { get; set; }
private List<FieldDefinition>? Fields { get; set; }
protected async Task ModalOnShow()
{
Config = await _rpcClientService.CallAsync<SamsungServiceConfigDto>("SamsungService", "GetConfig");
Fields = SamsungFields.GetSettingsFields(Config);
StateHasChanged();
}
private async Task ApplyClick()
{
if (Config == null)
{
return;
}
FieldDefinition.UpdateObject(Fields, Config);
var result = await _rpcClientService.CallAsync<bool>("SamsungService", "UpdateConfig", Config);
}
}
@@ -0,0 +1,333 @@
@page "/Samsung"
@using ColorControl.Shared.Common
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.Shared.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@rendermode InteractiveServer
@inject RpcUiClientService _rpcClientService
@inject ProtectedLocalStorage _localStorage
<PageTitle>Samsung controller</PageTitle>
<SamsungSettings />
<SamsungServiceMenu Devices="Devices"></SamsungServiceMenu>
<div class="d-flex align-items-center">
<h1>Samsung controller</h1>
<div class="d-flex ms-auto">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle me-2" data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">
Buttons
</button>
<ul class="dropdown-menu">
@foreach (var button in Enum.GetValues<ButtonType>())
{
<li>
<a class="dropdown-item" @onclick="() => ButtonOnClick(button)">@button.GetDescription()</a>
</li>
}
</ul>
</div>
@if (InvokableActions != null)
{
<div class="btn-group">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle me-2" data-bs-toggle="dropdown" aria-expanded="false">
Expert
</button>
<ul class="dropdown-menu">
@foreach (var action in InvokableActions)
{
if (action.Name == "ServiceMenu")
{
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#serviceMenuModal">Service menu</a></li>
}
else
{
<li><a class="dropdown-item" @onclick="() => ActionOnclick(action)">@action.Name</a></li>
}
}
</ul>
</div>
}
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#settingsModal">
Settings
</button>
</div>
</div>
<SamsungDevicePage Device="CurrentDevice" AfterApply="AfterDeviceApplied" />
<SamsungPresetPage PresetParam="CurrentPreset" Devices="Devices" AfterApply="AfterPresetApplied" />
<ConfirmModal Title="Delete preset" Message="Are you sure you want to delete the preset?" OnConfirm="PresetOnDeleteConfirmed" />
@if (Devices == null)
{
<div>Loading...</div>
}
else
{
<div class="mb-2 d-flex align-items-center">
<h2>Devices</h2>
<div class="d-flex ms-auto">
<button type="button" class="btn btn-sm btn-primary me-2" disabled="@IsRefreshDisabled" @onclick="RefreshDevicesOnClick">
@if (IsRefreshDisabled)
{
<span class="spinner-border spinner-border-sm me-1" aria-hidden="true"></span>
}
<span role="status">Refresh</span>
</button>
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#deviceModal" @onclick="() => AddDeviceOnClick()">
Add
</button>
</div>
</div>
<div class="row mb-3">
@if (!Devices.Any())
{
<span>No devices found</span>
}
@foreach (var device in Devices)
{
<div class="col-lg-6 mb-3">
<div class="card h-100">
<div class="card-header d-flex align-items-baseline">
<span>
@device.Name
@if (device.IsConnected)
{
<span class="ms-2 badge text-bg-success" title="Device is connected">Connected</span>
}
else
{
<span class="ms-2 badge text-bg-danger" title="Device is disconnected">Disconnected</span>
}
@if (device.IsSelected)
{
<span class="ms-2 badge text-bg-primary" title="Global device">Primary</span>
}
</span>
<div class="d-flex ms-auto">
@if (!device.IsSelected)
{
<button class="btn btn-sm btn-secondary ms-2" @onclick="() => SetAsPrimaryOnClick(device)">Set as primary</button>
}
<button type="button" class="btn btn-sm btn-primary ms-2" data-bs-toggle="modal" data-bs-target="#deviceModal" @onclick="() => DeviceOnClick(device)">
Settings
</button>
</div>
</div>
<div class="card-body">
<small class="text-muted">@device.IpAddress (@(device.MacAddress ?? "No MAC-address entered"))</small>
</div>
</div>
</div>
}
</div>
}
@if (Presets == null)
{
<div>Loading...</div>
}
else
{
<div class="mb-2 d-flex align-items-center">
<h2>Presets</h2>
<div class="d-flex ms-auto">
<select class="form-select me-2" id="presetOrder" @bind="PresetOrder" @bind:after="PresetOrderChanged">
@foreach (var order in Enum.GetValues<PresetOrder>())
{
<option value="@order">Order by: @order.GetDescription()</option>
}
</select>
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => AddPresetOnClick()">
Add
</button>
</div>
</div>
<div class="row mb-3">
<div class="col">
<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>
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="mb-1">@preset.name <small class="text-muted">@GetDeviceName(preset)</small></h5>
@if (preset.Triggers.Any(t => t.Trigger != PresetTriggerType.None))
{
<div><small class="text-muted">Trigger: @preset.Triggers[0].ToString()</small></div>
}
<div><small class="text-muted">@preset.shortcut</small></div>
</div>
<span>
<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>
</a>
}
</div>
</div>
</div>
}
@code {
private List<SamsungPreset>? Presets;
private List<SamsungDeviceDto>? Devices;
private List<InvokableActionDto<SamsungPreset>>? InvokableActions;
private SamsungServiceConfigDto? Config;
private SamsungPreset? CurrentPreset;
private SamsungDeviceDto? CurrentDevice;
private bool IsRefreshDisabled;
private PresetOrder PresetOrder;
protected override async Task OnInitializedAsync()
{
Config = await _rpcClientService.CallAsync<SamsungServiceConfigDto>("SamsungService", "GetConfig");
await RefreshPresets();
Devices = await _rpcClientService.CallAsync<List<SamsungDeviceDto>>("SamsungService", "GetDevices");
InvokableActions = await _rpcClientService.CallAsync<List<InvokableActionDto<SamsungPreset>>>("SamsungService", "GetInvokableActions");
}
protected override async Task OnAfterRenderAsync(bool firstTime)
{
if (firstTime)
{
PresetOrder = await _localStorage.TryGet<PresetOrder>("SamsungPresetOrder");
Presets = Presets?.OrderPresetsBy(PresetOrder);
StateHasChanged();
}
}
private async Task PresetOnClick(SamsungPreset preset)
{
await _rpcClientService.CallAsync<bool>("SamsungService", "ApplyPresetWithId", preset.id);
}
private async Task PresetOnEditClick(SamsungPreset preset)
{
CurrentPreset = preset;
}
private async Task PresetOnCloneClick(SamsungPreset preset)
{
CurrentPreset = new SamsungPreset(preset);
CurrentPreset.SetCloneValues();
}
private async Task PresetOnDeleteClick(SamsungPreset preset)
{
CurrentPreset = preset;
}
private async Task PresetOnDeleteConfirmed()
{
if (CurrentPreset == null)
{
return;
}
await _rpcClientService.CallAsync<bool>("SamsungService", "DeletePreset", CurrentPreset.id);
await RefreshPresets();
StateHasChanged();
}
private async Task DeviceOnClick(SamsungDeviceDto device)
{
CurrentDevice = device;
}
private async Task AddDeviceOnClick()
{
CurrentDevice = new SamsungDeviceDto { IsCustom = true };
}
private async Task AddPresetOnClick()
{
CurrentPreset = new SamsungPreset();
}
private async Task AfterPresetApplied(SamsungPreset preset)
{
await RefreshPresets();
StateHasChanged();
}
private async Task AfterDeviceApplied(SamsungDeviceDto device)
{
Devices = await _rpcClientService.CallAsync<List<SamsungDeviceDto>>("SamsungService", "GetDevices");
StateHasChanged();
}
private async Task RefreshDevicesOnClick(MouseEventArgs e)
{
IsRefreshDisabled = true;
try
{
await _rpcClientService.CallAsync<bool>("SamsungService", "RefreshDevices");
Devices = await _rpcClientService.CallAsync<List<SamsungDeviceDto>>("SamsungService", "GetDevices");
}
finally
{
IsRefreshDisabled = false;
}
StateHasChanged();
}
private async Task ActionOnclick(InvokableActionDto<SamsungPreset> action)
{
await _rpcClientService.CallAsync<bool>("SamsungService", "ExecuteInvokableAction", action);
}
private string GetDeviceName(SamsungPreset preset)
{
if (string.IsNullOrEmpty(preset.DeviceMacAddress))
{
return "Global";
}
return Devices?.FirstOrDefault(d => d.MacAddress == preset.DeviceMacAddress)?.Name ?? preset.DeviceMacAddress;
}
private async Task SetAsPrimaryOnClick(SamsungDeviceDto device)
{
var result = await _rpcClientService.CallAsync<bool>("SamsungService", "SetSelectedDevice", device);
if (result)
{
Devices = await _rpcClientService.CallAsync<List<SamsungDeviceDto>>("SamsungService", "GetDevices");
}
}
private async Task PresetOrderChanged()
{
await _localStorage.SetAsync("SamsungPresetOrder", PresetOrder);
await RefreshPresets();
}
private async Task RefreshPresets()
{
Presets = await _rpcClientService.CallAsync<List<SamsungPreset>>("SamsungService", "GetPresets");
Presets = Presets?.OrderPresetsBy(PresetOrder);
}
private async Task ButtonOnClick(ButtonType button)
{
var preset = new SamsungPreset(button.ToString(), null, [button.ToString()]);
await _rpcClientService.CallAsync<bool>("SamsungService", "ApplyPreset", preset);
}
}
@@ -1,64 +0,0 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}