fix several issues, add remote controls to new UI

This commit is contained in:
Maassoft committed 2025-05-30 17:20:37 +02:00
1 parent c6c7e1ccc0
commit 827157f30f
91 files changed
+4140 -2530

No files matched your search

@@ -48,7 +48,7 @@
{
<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">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#displaySettings">
Display
</button>
</span>
@@ -264,10 +264,7 @@
</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>
<NullableRangeInput TValue="int" @bind-Value="Preset.SDRBrightness" Id="sdrBrightness" Label="SDR brightness" Unit="%" />
</div>
</div>
</div>
@@ -28,7 +28,7 @@
}
else
{
<div class="row mb-3">
<div class="row mb-2">
@if (!Displays.Any())
{
<span>No displays found</span>
@@ -54,6 +54,13 @@ else
}
else
{
<div class="row mb-3">
<div class="form-check form-switch ms-3">
<input class="form-check-input" type="checkbox" role="switch" id="usePerUserSettings" @bind="SelectedDisplay.UsePerUserSettings" @bind:after="AfterUsePerUseSettings">
<label class="form-check-label" for="usePerUserSettings">Use my settings for this device</label>
</div>
</div>
<div class="mb-2 d-flex align-items-center">
<h2>Display Associated Color profiles</h2>
<div class="d-flex ms-auto">
@@ -320,4 +327,19 @@ else
await SelectedDisplayChanged();
}
private async Task AfterUsePerUseSettings()
{
if (SelectedDisplay == null)
{
return;
}
var result = await _rpcClientService.CallAsync<bool>("ColorProfileService", "SetUsePerUserDisplayProfiles", SelectedDisplay.DeviceName, SelectedDisplay.UsePerUserSettings);
if (result)
{
await RefreshDisplays();
}
}
}
@@ -9,272 +9,235 @@
@rendermode Constants.RenderMode
@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-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(Preset?.id == 0 ? $"Settings for new preset" : $"Settings of preset {Preset?.IdOrName}")</h5>
<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 (RecentApps != null)
{
<div class="mb-2">
<label class="form-label" for="recentApp">Recent app</label>
<select class="form-select" id="recentApp" @bind="RecentAppFilename" @bind:after="RecentAppSelected">
@foreach (var app in RecentApps)
{
<option value="@app.Filename" title="@app.Path">@app.FriendlyName</option>
}
</select>
</div>
}
<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="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(Preset?.id == 0 ? $"Settings for new preset" : $"Settings of preset {Preset?.IdOrName}")</h5>
<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 (RecentApps != null)
{
<div class="mb-2">
<label class="form-label" for="recentApp">Recent app</label>
<select class="form-select" id="recentApp" @bind="RecentAppFilename" @bind:after="RecentAppSelected">
@foreach (var app in RecentApps)
{
<option value="@app.Filename" title="@app.Path">@app.FriendlyName</option>
}
</select>
</div>
}
<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="accordion accordion-flush" id="presetAccordion">
<StartProgramInput @bind-Path="Preset.Path" @bind-Parameters="Preset.Parameters" @bind-Path:after="PathChanged"></StartProgramInput>
<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 class="accordion accordion-flush" id="presetAccordion">
</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="#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>
}
</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>
}
</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;
private GamePreset? Preset;
[Parameter]
public GamePreset? PresetParam { get; set; }
[Parameter]
public GamePreset? PresetParam { get; set; }
[Parameter]
public Func<GamePreset, Task>? AfterApply { get; set; }
[Parameter]
public Func<GamePreset, Task>? AfterApply { get; set; }
public List<string> CpuAffinityLabels = Enumerable.Range(0, Environment.ProcessorCount).Select(i => $"CPU #{i}").ToList();
private List<GameApp>? RecentApps;
private string RecentAppFilename = "";
public List<string> CpuAffinityLabels = Enumerable.Range(0, Environment.ProcessorCount).Select(i => $"CPU #{i}").ToList();
private List<GameApp>? RecentApps;
private string RecentAppFilename = "";
public bool SelectingFile { get; set; }
public bool SelectingFile { get; set; }
private string WasValidated = "";
private bool IsVisible;
private string WasValidated = "";
private bool IsVisible;
protected override async Task OnParametersSetAsync()
{
if (PresetParam == null)
{
return;
}
protected override async Task OnParametersSetAsync()
{
if (PresetParam == null)
{
return;
}
Preset = PresetParam.CloneWithId();
Preset = PresetParam.CloneWithId();
if (Preset.id == 0)
{
RecentApps = await _rpcClientService.CallAsync<List<GameApp>>("GameService", "GetMruApps", true);
RecentAppFilename = "";
if (Preset.id == 0)
{
RecentApps = await _rpcClientService.CallAsync<List<GameApp>>("GameService", "GetMruApps", true);
RecentAppFilename = "";
if (RecentApps != null)
{
RecentApps.Insert(0, new GameApp { Filename = "", FriendlyName = "Choose a recent app to fill values" });
}
}
else
{
RecentApps = null;
}
}
if (RecentApps != null)
{
RecentApps.Insert(0, new GameApp { Filename = "", FriendlyName = "Choose a recent app to fill values" });
}
}
else
{
RecentApps = null;
}
}
private async Task ApplyClick(MouseEventArgs e)
{
if (Preset == null)
{
return;
}
private async Task ApplyClick(MouseEventArgs e)
{
if (Preset == null)
{
return;
}
if (!IsValid())
{
WasValidated = "was-validated";
return;
}
if (!IsValid())
{
WasValidated = "was-validated";
return;
}
var result = await _rpcClientService.CallAsync<bool>("GameService", "UpdatePreset", Preset);
var result = await _rpcClientService.CallAsync<bool>("GameService", "UpdatePreset", Preset);
if (AfterApply != null)
{
await AfterApply.Invoke(Preset);
}
if (AfterApply != null)
{
await AfterApply.Invoke(Preset);
}
IsVisible = false;
}
IsVisible = false;
}
private bool IsValid()
{
return !string.IsNullOrEmpty(Preset?.name) && !string.IsNullOrEmpty(Preset?.Path);
}
private bool IsValid()
{
return !string.IsNullOrEmpty(Preset?.name) && !string.IsNullOrEmpty(Preset?.Path);
}
private async Task SelectFileOnClick(MouseEventArgs e)
{
if (Preset == null)
{
return;
}
private void RecentAppSelected()
{
if (RecentApps == null || Preset == null)
{
return;
}
SelectingFile = true;
try
{
var options = new RpcUiClientOptions
{
Timeout = 60000
};
var app = RecentApps.FirstOrDefault(a => a.Filename == RecentAppFilename);
var result = await _rpcClientService.CallWithOptionsAsync<string>("GameService", "SelectPath", options);
if (app != null)
{
Preset.name = app.FriendlyName;
Preset.Path = app.Path;
}
}
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;
}
}
private void PathChanged()
{
if (Preset == null)
{
return;
}
private void RecentAppSelected()
{
if (RecentApps == null || Preset == null)
{
return;
}
var app = RecentApps.FirstOrDefault(a => a.Filename == RecentAppFilename);
if (app != null)
{
Preset.name = app.FriendlyName;
Preset.Path = app.Path;
}
}
private void ModalOnFocus(FocusEventArgs e)
{
if (!IsVisible)
{
WasValidated = "";
IsVisible = true;
}
}
if (Preset.Path != null && string.IsNullOrEmpty(Preset.name))
{
Preset.name = Path.GetFileNameWithoutExtension(Preset.Path);
}
}
}
@@ -4,7 +4,7 @@
@rendermode Constants.RenderMode
<FieldsModal ModalId="@ModalId" Fields="Fields" Title="@Title" OnConfirm="ConfirmClick"></FieldsModal>
<FieldsModal ModalId="@ModalId" Fields="Fields" Title="@Title" OnConfirm="ConfirmClick" NextModalId="@NextModalId"></FieldsModal>
@code {
[Parameter]
@@ -19,6 +19,9 @@
[Parameter]
public string ModalId { get; set; } = "confirmModal";
[Parameter]
public string? NextModalId { get; set; }
[Parameter]
public List<FieldDefinition>? ExtraFields { get; set; }
@@ -4,8 +4,6 @@
@rendermode Constants.RenderMode
@inject NavigationManager _navigationManager;
@if (Fields == null)
{
return;
@@ -106,16 +104,7 @@
{
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>
<RangeInput TValue="int" @bind-Value="field.IntValue" Id="@fieldId" Label="@field.Label" Min="@((int)field.MinValue)" Max="@((int)field.MaxValue)" SubLabel="@field.SubLabel" />
}
}
@@ -3,8 +3,7 @@
@using ColorControl.Shared.Common;
@rendermode Constants.RenderMode
@inject NavigationManager _navigationManager;
@inject JSHelper jsHelper;
<div class="modal" id="@ModalId" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" @onfocus="ModalFocus">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
@@ -41,6 +40,9 @@
[Parameter]
public string? ModalId { get; set; }
[Parameter]
public string? NextModalId { get; set; }
[Parameter]
public string? ConfirmCaption { get; set; } = "Yes";
@@ -120,8 +122,13 @@
}
}
private void OnClose(MouseEventArgs e)
private async Task OnClose(MouseEventArgs e)
{
IsVisible = false;
if (!string.IsNullOrEmpty(NextModalId))
{
await jsHelper.OpenModal(NextModalId);
}
}
}
@@ -0,0 +1,73 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.EventDispatcher
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@* @rendermode Constants.RenderMode *@
@typeparam TValue where TValue : struct, IComparable, IFormattable, IConvertible, IComparable<TValue>, IEquatable<TValue>
@if (Id != null)
{
<div class="mb-2">
<label class="form-label @(ExtraLabelStyling ?? "")" for="@Id">@Label (@Value@Unit)</label>
<input class="form-range" type="range" min="@(Values?.Min() ?? Min)" max="@(Values?.Max() ?? Max)" @bind="Value" id="@Id" @oninput="e => RangeOnChange(e)">
</div>
}
@code {
[Parameter]
public TValue? Value { get; set; }
[Parameter]
public string? Label { get; set; }
[Parameter]
public string? Unit { get; set; }
[Parameter]
public string? Id { get; set; }
[Parameter]
public TValue? Min { get; set; }
[Parameter]
public TValue? Max { get; set; }
[Parameter]
public List<TValue>? Values { get; set; }
[Parameter]
public EventCallback<TValue?> ValueChanged { get; set; }
[Parameter]
public Func<TValue?, string>? LabelStylingFunc { get; set; }
private string? ExtraLabelStyling;
protected override void OnParametersSet()
{
ExtraLabelStyling = LabelStylingFunc?.Invoke(Value);
Min ??= (TValue)Convert.ChangeType(0, typeof(TValue));
Max ??= (TValue)Convert.ChangeType(100, typeof(TValue));
}
private async Task RangeOnChange(ChangeEventArgs eventArgs)
{
if (typeof(TValue) == typeof(int))
{
Value = (TValue)Convert.ChangeType(int.Parse(eventArgs.Value?.ToString() ?? "0"), typeof(TValue));
}
else if (typeof(TValue) == typeof(uint))
{
Value = (TValue)Convert.ChangeType(uint.Parse(eventArgs.Value?.ToString() ?? "0"), typeof(TValue));
}
await ValueChanged.InvokeAsync(Value);
ExtraLabelStyling = LabelStylingFunc?.Invoke(Value);
}
}
@@ -0,0 +1,116 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.EventDispatcher
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@* @rendermode Constants.RenderMode *@
@typeparam TValue where TValue : struct, IComparable, IFormattable, IConvertible, IComparable<TValue>, IEquatable<TValue>
@if (Id != null)
{
<div class="mb-2">
<label class="form-label @(ExtraLabelStyling ?? "")" for="@Id">@Label: @GetValueDisplay()@Unit</label>
<input class="form-range" type="range" step="@Step" min="@(Values?.Min() ?? Min)" max="@(Values?.Max() ?? Max)" @bind="Value" id="@Id" @oninput="e => RangeOnChange(e)">
@if (SubLabel != null)
{
<div id="@(Id)_help" class="form-text">
@SubLabel
</div>
}
</div>
}
@code {
[Parameter]
public TValue Value { get; set; }
[Parameter]
public string? Label { get; set; }
[Parameter]
public string? SubLabel { get; set; }
[Parameter]
public string? Unit { get; set; }
[Parameter]
public string? UnitString { get; set; }
[Parameter]
public string? Id { get; set; }
[Parameter]
public TValue? Min { get; set; }
[Parameter]
public TValue? Max { get; set; }
[Parameter]
public TValue? Step { get; set; }
[Parameter]
public List<TValue>? Values { get; set; }
[Parameter]
public EventCallback<TValue> ValueChanged { get; set; }
[Parameter]
public Func<TValue, string>? LabelStylingFunc { get; set; }
[Parameter]
public Func<TValue, string>? CustomLabelFunc { get; set; }
private string? ExtraLabelStyling;
protected override void OnParametersSet()
{
ExtraLabelStyling = LabelStylingFunc?.Invoke(Value);
Min ??= (TValue)Convert.ChangeType(0, typeof(TValue));
Max ??= (TValue)Convert.ChangeType(100, typeof(TValue));
Step ??= (TValue)Convert.ChangeType(1, typeof(TValue));
}
private string? GetValueDisplay()
{
if (CustomLabelFunc != null)
{
return CustomLabelFunc(Value);
}
if (string.IsNullOrEmpty(UnitString))
{
return Value.ToString();
}
if (Value is uint uintValue)
{
return uintValue.ToUnitString(UnitString);
}
if (Value is int intValue)
{
return intValue.ToSignedUnitString(UnitString);
}
return Value.ToString();
}
private async Task RangeOnChange(ChangeEventArgs eventArgs)
{
if (typeof(TValue) == typeof(int))
{
Value = (TValue)Convert.ChangeType(int.Parse(eventArgs.Value?.ToString() ?? "0"), typeof(TValue));
}
else if (typeof(TValue) == typeof(uint))
{
Value = (TValue)Convert.ChangeType(uint.Parse(eventArgs.Value?.ToString() ?? "0"), typeof(TValue));
}
await ValueChanged.InvokeAsync(Value);
ExtraLabelStyling = LabelStylingFunc?.Invoke(Value);
}
}
@@ -19,7 +19,8 @@
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
</button>
<ul class="dropdown-menu">
@foreach (var modifier in Modifiers) {
@foreach (var modifier in Modifiers)
{
<li>
<a class="dropdown-item" @onclick="() => ModifierOnClick(modifier.Key)">
<input class="form-check-input me-2" type="checkbox" id="flexCheckChecked" checked="@modifier.Value" @onchange="(e) => ModifierKeyChanged(e, modifier.Key)">@modifier.Key
@@ -44,9 +45,9 @@
private Dictionary<string, bool> Modifiers = new() {
{ "Shift", false },
{ "Control", false },
{ "Alt", false },
{ "Win", false }
};
{ "Alt", false },
{ "Win", false }
};
protected override void OnParametersSet()
{
@@ -0,0 +1,64 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.EventDispatcher
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@inject RpcUiClientService _rpcClientService
<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="Path" required @onblur="() => PathChanged.InvokeAsync(Path)" />
<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="Parameters" @bind:after="() => ParametersChanged.InvokeAsync(Parameters)" />
</div>
@code {
[Parameter]
public string? Path { get; set; }
[Parameter]
public string? Parameters { get; set; }
[Parameter]
public EventCallback<string?> PathChanged { get; set; }
[Parameter]
public EventCallback<string?> ParametersChanged { get; set; }
private bool SelectingFile;
private async Task SelectFileOnClick(MouseEventArgs e)
{
SelectingFile = true;
try
{
var options = new RpcUiClientOptions
{
Timeout = 60000
};
var result = await _rpcClientService.CallWithOptionsAsync<string>("GameService", "SelectPath", options);
if (result != null)
{
Path = result;
await PathChanged.InvokeAsync(Path);
}
}
finally
{
SelectingFile = false;
}
}
}
@@ -12,22 +12,21 @@
@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)
@if (NvPresets != null || SamsungPresets != null || LgPresets != 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">
<form class="dropdown-menu p-4 dropdown-menu-end" style="width: 360px">
@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="">Select preset</option>
<select class="form-select" multiple id="nvPreset" @onchange="NvPresetChange">
@foreach (var preset in NvPresets)
{
<option value="@preset.id">@preset.IdOrName</option>
@@ -77,6 +76,9 @@
</div>
}
}
<StartProgramInput @bind-Path="StartProgramPath" @bind-Parameters="StartProgramParameters"></StartProgramInput>
@if (ActionItems != null)
{
foreach (var customItem in ActionItems)
@@ -85,15 +87,15 @@
<label class="form-label" for="advancedCustomItem">@customItem.Name</label>
<select class="form-select" id="advancedCustomItem" @bind="customItem.SelectedActionName" @bind:after="() => AdvancedItemChanged(customItem)">
<option value="">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>
}
@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>
@@ -126,10 +128,7 @@
<button type="button" class="btn btn-primary btn-sm" data-bs-dismiss="dropdown" title="Move right" disabled="@(stepIndex == PresetSteps.Count - 1)" @onclick="() => MoveRightOnClick(stepIndex)">&gt;</button>
<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>
<RangeInput TValue="int" @bind-Value="step.Delay" Id="delay" Label="Delay" Unit="ms" Max="10000" @bind-Value:after="() => AfterDelayChanged()" />
</form>
index++;
@@ -212,7 +211,6 @@
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; }
@@ -222,6 +220,11 @@
private List<FieldDefinition>? ActionFields;
private string? StartProgramPath;
private string? StartProgramParameters;
private List<int> NvPresetsToAdd = [];
protected override void OnParametersSet()
{
if (Steps != null)
@@ -295,7 +298,7 @@
private async Task AddPresetOnClick()
{
AddPreset(NvPresets, "NvPreset", NvPresetToAddId);
NvPresetsToAdd.ForEach(id => AddPreset(NvPresets, "NvPreset", id));
AddPreset(SamsungPresets, "SamsungPreset", SamsungPresetToAddId);
AddPreset(LgPresets, "LgPreset", LgPresetToAddId);
@@ -328,6 +331,8 @@
}
}
AddStartProgram();
await EmitSteps();
}
@@ -343,6 +348,22 @@
PresetSteps.Add(PresetStep.Parse($"{type}({preset.name})"));
}
private void AddStartProgram()
{
if (string.IsNullOrWhiteSpace(StartProgramPath))
{
return;
}
var path = StartProgramPath;
if (!string.IsNullOrWhiteSpace(StartProgramParameters))
{
path += $"; {string.Join("; ", StartProgramParameters.Split(' '))}";
}
PresetSteps.Add(PresetStep.Parse($"StartProgram({path})"));
}
private async Task EmitSteps()
{
if (Steps != null)
@@ -427,4 +448,13 @@
return field;
}
private void NvPresetChange(ChangeEventArgs eventArgs)
{
if (eventArgs.Value is IEnumerable<string> items)
{
NvPresetsToAdd.Clear();
NvPresetsToAdd.AddRange(items.Select(i => int.Parse(i)));
}
}
}
@@ -7,6 +7,14 @@
@if (Triggers != null)
{
if (!Triggers.Any())
{
<div>
<label class="form-label">No triggers found. Click to add:</label>
<button class="btn btn-primary btn-sm" type="button" @onclick="() => Triggers.Add(new PresetTrigger())">Add</button>
</div>
}
foreach (var trigger in Triggers)
{
<div class="mb-2">
@@ -9,8 +9,6 @@
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager;
@inject NotificationService _notificationService
@inject ProtectedSessionStorage _sessionStorage
@@ -6,7 +6,6 @@
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager;
@inject IHttpContextAccessor _httpContextAccessor
@@ -3,14 +3,13 @@
@using ColorControl.UI.Services
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common
@using ColorControl.UI.Components.Pages.Generic
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager;
<div class="modal" id="deviceModal" tabindex="-1">
<div class="modal" id="lgDeviceModal" tabindex="-1">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
@@ -73,7 +72,7 @@
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#deviceOptions">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#deviceOptions">
Device options
</button>
</h2>
@@ -165,6 +164,10 @@
</form>
</div>
<div class="modal-footer">
@if (Device?.IsCustom == true)
{
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#removeDeviceModal">Remove</button>
}
<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>
@@ -172,6 +175,8 @@
</div>
</div>
<ConfirmModal ModalId="removeDeviceModal" Title="Remove device" Message="Are you sure you want to remove this device?" OnConfirm="DeviceOnRemoveConfirmed" NextModalId="lgDeviceModal" />
@code {
private LgDeviceDto? _device;
@@ -208,6 +213,21 @@
}
}
private async Task DeviceOnRemoveConfirmed()
{
if (Device == null)
{
return;
}
var result = await _rpcClientService.CallAsync<bool>("LgService", "RemoveDevice", Device);
if (AfterApply != null)
{
await AfterApply.Invoke(Device);
}
}
private async Task TestConnectionClick(MouseEventArgs e)
{
IsTestingConnection = true;
@@ -9,8 +9,6 @@
@rendermode Constants.RenderMode
@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-scrollable modal-dialog-centered">
@@ -74,7 +72,7 @@
</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>
<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" />
@@ -4,6 +4,7 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.LG
@using ColorControl.UI.Components.Pages.RemoteControl
@using ColorControl.UI.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Generics
@@ -22,6 +23,9 @@
<div class="d-flex align-items-center">
<h1>LG controller</h1>
<div class="d-flex ms-auto">
<button type="button" class="btn btn-sm btn-primary me-2" data-bs-toggle="offcanvas" data-bs-target="#rcCanvasRight" aria-controls="rcCanvasRight">
Remote control
</button>
<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
@@ -64,7 +68,18 @@
</div>
</div>
<LgDevicePage Device="CurrentDevice" />
<div class="offcanvas offcanvas-end" style="width: 280px" data-bs-scroll="true" tabindex="-1" id="rcCanvasRight" aria-labelledby="rcCanvasRightRightLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="rcCanvasRightRightLabel">Remote control</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<GenericRcPage ServiceName="LgService" Device="CurrentDevice" TDevice="LgDeviceDto" TPreset="LgPreset" TButtonEnum="ButtonType" Presets="RcPresets"></GenericRcPage>
</div>
</div>
<LgDevicePage Device="CurrentDevice" AfterApply="AfterDeviceApplied" />
<LgPresetPage PresetParam="CurrentPreset" Devices="Devices" AfterApply="AfterPresetApplied" />
<ConfirmModal Title="Delete preset" Message="Are you sure you want to delete the preset?" OnConfirm="PresetOnDeleteConfirmed" />
@@ -84,7 +99,7 @@ else
}
<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()">
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#lgDeviceModal" @onclick="() => AddDeviceOnClick()">
Add
</button>
</div>
@@ -119,7 +134,7 @@ else
{
<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)">
<button type="button" class="btn btn-sm btn-primary ms-2" data-bs-toggle="modal" data-bs-target="#lgDeviceModal" @onclick="() => DeviceOnClick(device)">
Settings
</button>
</div>
@@ -221,6 +236,7 @@ else
private InvokableActionDto<LgPreset>? CurrentInvokableAction;
private PresetOrder PresetOrder;
private List<LgPreset> RcPresets = [LgPresets.InStartPreset, LgPresets.EzAdjustPreset];
protected override async Task OnInitializedAsync()
{
@@ -297,7 +313,13 @@ else
StateHasChanged();
}
private async Task RefreshDevicesOnClick(MouseEventArgs e)
private async Task AfterDeviceApplied(LgDeviceDto device)
{
Devices = await _rpcClientService.CallAsync<List<LgDeviceDto>>("LgService", "GetDevices");
StateHasChanged();
}
private async Task RefreshDevicesOnClick()
{
IsRefreshDisabled = true;
try
@@ -6,8 +6,6 @@
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager;
<PageTitle>Logging</PageTitle>
@@ -7,8 +7,6 @@
@rendermode Constants.RenderMode
@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">
@@ -1,5 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Services;
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common;
@@ -26,14 +27,14 @@
<div class="modal-body">
<form class="row g-1">
@if (ProfileNames != null)
@if (Profiles != 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)
@foreach (var profile in Profiles)
{
<option value="@profileName">@profileName</option>
<option value="@profile.Name" title="@string.Join(", ", profile.Apps)">@profile.Name</option>
}
</select>
</div>
@@ -64,10 +65,8 @@
{
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>
<RangeInput TValue="uint" @bind-Value="setting.Value" Id="@setting.SettingId.ToString()" Label="@setting.SettingText" Values="setting.Values.Select(v => v.Value).ToList()"
LabelStylingFunc="value => RangeLabelStyleFunc(value, setting)" />
}
else if (setting.Values != null)
{
@@ -137,7 +136,7 @@
private List<GroupedSettings>? Groups { get; set; }
private List<NvSettingItemDto>? DriverSettings { get; set; }
private List<string>? ProfileNames { get; set; }
private List<NvProfile>? Profiles { get; set; }
private string? CurrentProfileName { get; set; }
private bool IsVisible { get; set; }
@@ -244,7 +243,7 @@
private async Task OnlyFoundProgramsOnChange()
{
ProfileNames = await _rpcClientService.CallAsync<List<string>>("NvService", "GetDriverProfileNames", OnlyFoundPrograms);
Profiles = await _rpcClientService.CallAsync<List<NvProfile>>("NvService", "GetDriverProfileNames", OnlyFoundPrograms);
}
private async Task OnFocus(FocusEventArgs e)
@@ -252,8 +251,18 @@
if (!IsVisible)
{
IsVisible = true;
ProfileNames ??= await _rpcClientService.CallAsync<List<string>>("NvService", "GetDriverProfileNames", OnlyFoundPrograms);
Profiles ??= await _rpcClientService.CallAsync<List<NvProfile>>("NvService", "GetDriverProfileNames", OnlyFoundPrograms);
await LoadDriverSettings();
}
}
private void RangeOnChange(ChangeEventArgs eventArgs, NvSettingItemDto s)
{
s.Value = uint.Parse(eventArgs.Value?.ToString() ?? "0");
}
private string RangeLabelStyleFunc(uint value, NvSettingItemDto setting)
{
return value != setting.DefaultValue && value != NvSettingConstants.UnsetDwordValue ? "text-warning fw-bold" : "";
}
}
@@ -1,5 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Services;
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common;
@@ -53,47 +54,33 @@
@if (OverclockSettings.Type is NvGpuOcType.Curve or NvGpuOcType.Offset)
{
<div class="mb-2">
<label class="form-label" for="coreOffset">Core offset in MHz: @OverclockSettings.GraphicsOffsetMHz.ToSignedUnitString() (Min: @GpuInfo.MinCoreDeltaInMHz.ToSignedUnitString(), Max: @GpuInfo.MaxCoreDeltaInMHz.ToSignedUnitString())</label>
<input class="form-range" type="range" step="@GpuInfo.MinCoreStepInMHz" min="@GpuInfo.CorrectedMinCoreDeltaInMHz" max="@GpuInfo.MaxCoreDeltaInMHz" @bind="OverclockSettings.GraphicsOffsetMHz" id="coreOffset">
</div>
<div class="mb-2">
<label class="form-label" for="memoryOffset">Memory offset in MHz: @OverclockSettings.MemoryOffsetMHz.ToSignedUnitString() (Min: @GpuInfo.MinMemoryDeltaInMHz.ToSignedUnitString(), Max: @GpuInfo.MaxMemoryDeltaInMHz.ToSignedUnitString())</label>
<input class="form-range" type="range" step="1" min="@GpuInfo.MinMemoryDeltaInMHz" max="@GpuInfo.MaxMemoryDeltaInMHz" @bind="OverclockSettings.MemoryOffsetMHz" id="memoryOffset">
</div>
<RangeInput TValue="int" Min="@GpuInfo.CorrectedMinCoreDeltaInMHz" Max="@GpuInfo.MaxCoreDeltaInMHz" Step="@GpuInfo.MinCoreStepInMHz" @bind-Value="OverclockSettings.GraphicsOffsetMHz" Id="coreOffset" Label="Core offset" UnitString="MHz"
SubLabel="@($"Min: {GpuInfo.MinCoreDeltaInMHz.ToSignedUnitString()}, Max: {GpuInfo.MaxCoreDeltaInMHz.ToSignedUnitString()}")" />
<RangeInput TValue="int" Min="@GpuInfo.MinMemoryDeltaInMHz" Max="@GpuInfo.MaxMemoryDeltaInMHz" @bind-Value="OverclockSettings.MemoryOffsetMHz" Id="memoryOffset" Label="Memory offset" UnitString="MHz"
SubLabel="@($"Min: {GpuInfo.MinMemoryDeltaInMHz.ToSignedUnitString()}, Max: {GpuInfo.MaxMemoryDeltaInMHz.ToSignedUnitString()}")" />
}
@if (OverclockSettings.Type is NvGpuOcType.Curve or NvGpuOcType.BoostLock)
{
<div class="mb-2">
<label class="form-label" for="maxCoreFrequency">Maximum core frequency MHz: @OverclockSettings.MaximumFrequencyMHz.ToUnitString() (Min: @GpuInfo.MinCurveFrequencyInMHz.ToUnitString(), Max: @GpuInfo.MaxCurveFrequencyInMHz.ToUnitString())</label>
<input class="form-range" type="range" step="@GpuInfo.MinCoreStepInMHz" min="@GpuInfo.MinCurveFrequencyInMHz" max="@GpuInfo.MaxCurveFrequencyInMHz" @bind="OverclockSettings.MaximumFrequencyMHz" id="maxCoreFrequency">
<div id="maxCoreFrequencyHelp" class="form-text">Setting this value to anything else than '0' will limit the maximum frequency of the curve.</div>
</div>
<div class="mb-2">
<label class="form-label" for="maxCoreVoltage">Maximum core voltage in millivolt: @OverclockSettings.MaximumVoltageMv.ToUnitString("mv") (Min: @GpuInfo.MinCurveVoltageInMv.ToUnitString("mv"), Max: @GpuInfo.MaxCurveVoltageInMv.ToUnitString("mv"))</label>
<input class="form-range" type="range" step="5" min="@GpuInfo.MinCurveVoltageInMv" max="@GpuInfo.MaxCurveVoltageInMv" @bind="OverclockSettings.MaximumVoltageMv" id="maxCoreVoltage">
<div id="maxCoreFrequencyHelp" class="form-text">Setting this value to anything else than '0' will limit the maximum voltage (and also the frequency) of the curve.</div>
</div>
<RangeInput TValue="uint" Min="@GpuInfo.MinCurveFrequencyInMHz" Max="@GpuInfo.MaxCurveFrequencyInMHz" Step="@((uint)GpuInfo.MinCoreStepInMHz)" @bind-Value="OverclockSettings.MaximumFrequencyMHz" Id="maxCoreFrequency" Label="Maximum core frequency" UnitString="MHz"
SubLabel="@($"Setting this value to anything else than '0' will limit the maximum frequency of the curve. Min: {GpuInfo.MinCurveFrequencyInMHz.ToSignedUnitString()}, Max: {GpuInfo.MaxCurveFrequencyInMHz.ToSignedUnitString()}")" />
<RangeInput TValue="uint" Min="@GpuInfo.MinCurveVoltageInMv" Max="@GpuInfo.MaxCurveVoltageInMv" Step="5" @bind-Value="OverclockSettings.MaximumVoltageMv" Id="maxCoreVoltage" Label="Maximum core voltage" UnitString="mv"
SubLabel="@($"Setting this value to anything else than '0' will limit the maximum voltage (and also the frequency) of the curve. Min: {GpuInfo.MinCurveVoltageInMv.ToUnitString("mv")}, Max: {GpuInfo.MaxCurveVoltageInMv.ToUnitString("mv")}")" />
}
@if (OverclockSettings.Type != NvGpuOcType.None)
{
<div class="mb-2">
<label class="form-label" for="voltageBoost">Voltage boost in %: @OverclockSettings.VoltageBoostPercent.ToUnitString("%")</label>
<input class="form-range" type="range" step="1" min="0" max="100" @bind="OverclockSettings.VoltageBoostPercent" id="voltageBoost">
</div>
<RangeInput TValue="uint" @bind-Value="OverclockSettings.VoltageBoostPercent" Id="voltageBoost" Label="Voltage boost" UnitString="%" />
@if (GpuInfo.DefaultPowerInMilliWatts > 0 && GpuInfo.MaxPowerInMilliWatts > 0 && GpuInfo.MinPowerInMilliWatts > 0 && GpuInfo.MaxPowerInMilliWatts > GpuInfo.MinPowerInMilliWatts)
{
<div class="mb-2">
<label class="form-label" for="powerLimitWatts">Power limit in watts: @(((uint)(OverclockSettings.PowerPCM / 100000f * GpuInfo.DefaultPowerInMilliWatts)).ToKiloUnitString(units: "W")) (@OverclockSettings.PowerPCM.ToKiloUnitString(units: "%")) (Min: @GpuInfo.MinPowerInMilliWatts.ToKiloUnitString(units: "W"), Max: @GpuInfo.MaxPowerInMilliWatts.ToKiloUnitString(units: "W"))</label>
<input class="form-range" type="range" step="1" min="@GpuInfo.MinimumPowerInPCM" max="@GpuInfo.MaximumPowerInPCM" @bind="OverclockSettings.PowerPCM" id="powerLimitWatts">
</div>
<RangeInput TValue="uint" Min="@GpuInfo.MinimumPowerInPCM" Max="@GpuInfo.MaximumPowerInPCM" @bind-Value="OverclockSettings.PowerPCM" Id="powerLimitWatts" Label="Power limit"
CustomLabelFunc='value => $"{((uint)(OverclockSettings.PowerPCM / 100000f * GpuInfo.DefaultPowerInMilliWatts)).ToKiloUnitString(units: "W")} ({OverclockSettings.PowerPCM.ToKiloUnitString(units: "%")})"'
SubLabel="@($"Min: {GpuInfo.MinPowerInMilliWatts.ToKiloUnitString(units: "W")}, Max: {GpuInfo.MaxPowerInMilliWatts.ToKiloUnitString(units: "W")}")" />
}
else
{
<div class="mb-2">
<label class="form-label" for="powerLimitPercentage">Power limit in percentage: @OverclockSettings.PowerPCM.ToKiloUnitString(units: "%") (Min: @GpuInfo.MinimumPowerInPCM.ToKiloUnitString(units: "%"), Max: @GpuInfo.MaximumPowerInPCM.ToKiloUnitString(units: "%")</label>
<input class="form-range" type="range" step="1" min="@GpuInfo.MinimumPowerInPCM" max="@GpuInfo.MaximumPowerInPCM" @bind="OverclockSettings.PowerPCM" id="powerLimitPercentage">
</div>
<RangeInput TValue="uint" Min="@GpuInfo.MinimumPowerInPCM" Max="@GpuInfo.MaximumPowerInPCM" @bind-Value="OverclockSettings.PowerPCM" Id="powerLimitPercentage" Label="Power limit"
CustomLabelFunc='value => $"{OverclockSettings.PowerPCM.ToKiloUnitString(units: "%")}"'
SubLabel="@($"Min: {GpuInfo.MinimumPowerInPCM.ToKiloUnitString(units: "%")}, Max: {GpuInfo.MaximumPowerInPCM.ToKiloUnitString(units: "%")}")" />
}
}
}
@@ -138,6 +125,11 @@
protected override async Task OnParametersSetAsync()
{
if (!IsVisible)
{
return;
}
if (_preset != Preset)
{
_preset = Preset;
@@ -0,0 +1,109 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.UI.Services;
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common;
@using static ColorControl.Shared.Common.NestedItemsBuilder
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
@inject JSHelper jsHelper
<div class="modal modal-lg" id="infoModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Info</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 (InfoItems != null)
{
@RenderItems(InfoItems)
}
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
@code {
private bool _visible;
[Parameter]
public bool Visible { get; set; }
private string? SelectedDisplayId { get; set; }
private List<NestedItem>? InfoItems;
protected override async Task OnParametersSetAsync()
{
if (_visible != Visible)
{
_visible = Visible;
if (Visible)
{
await GetSettingsAsync();
}
}
}
protected async Task GetSettingsAsync()
{
InfoItems = await _rpcClientService.CallAsync<List<NestedItem>>("NvService", "GetInfoItems");
if (InfoItems != null)
{
InfoItems.ForEach(i => i.Expanded = true);
}
}
RenderFragment<List<NestedItem>> RenderItems => items =>
@<div class="accordion accordion-flush" id="@items.GetHashCode()">
@foreach(var item in items)
{
@if (item.NestedItems.Count > 0)
{
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button py-2 @(item.Expanded ? "show" : "collapsed")" type="button" data-bs-toggle="collapse" data-bs-target="#@item.GetHashCode()" aria-expanded="false" aria-controls="flush-collapseOne" @onclick="() => OnItemClick(item)">
@item.Value
</button>
</h2>
<div id="@item.GetHashCode()" class="accordion-collapse collapse @(item.Expanded ? "show" : "")" data-bs-parent="#@items.GetHashCode()">
<div class="accordion-body">
@if (item.Expanded)
{
@RenderItems(item.NestedItems)
}
</div>
</div>
</div>
}
else
{
<div class="mt-1" style="margin-left: 1.25rem">
@item.Value
</div>
}
}
</div>
;
private async Task OnItemClick(NestedItem item)
{
await Task.Delay(400);
var classes = await jsHelper.GetElementClassName(item.GetHashCode().ToString());
item.Expanded = classes?.Contains(" show") == true;
}
}
@@ -1,17 +1,18 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.DisplayInfo
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.UI.Services;
@using ColorControl.UI.Services
@using ColorControl.UI.Components.Pages.Generic
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common;
@using ColorControl.Shared.Common
@using ColorControl.UI.Generics
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager;
@inject JSHelper jsHelper;
@inject JSHelper jsHelper
@inject ProtectedLocalStorage _localStorage
<div class="modal" id="presetModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" @onfocus="ModalOnFocus">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
@@ -109,7 +110,7 @@
{
<div class="mb-2">
<label class="form-label" for="activeResolution">Active resolution</label>
<select class="form-select" id="activeResolution" @bind="selectedActiveResolution">
<select class="form-select" id="activeResolution" @bind="selectedActiveResolution" @bind:after="() => AfterResolutionChange()">
<option value="0x0">Not set</option>
@foreach (var resolution in resolutions)
{
@@ -119,7 +120,7 @@
</div>
<div class="mb-2">
<label class="form-label" for="virtualResolution">Virtual resolution</label>
<select class="form-select" id="virtualResolution" @bind="selectedVirtualResolution">
<select class="form-select" id="virtualResolution" @bind="selectedVirtualResolution" @bind:after="() => AfterResolutionChange()">
<option value="0x0">Not set</option>
@foreach (var resolution in resolutions)
{
@@ -170,7 +171,7 @@
{
<div class="mb-2">
<label class="form-label" for="refreshRate">Refresh Rate</label>
<select class="form-select" id="refreshRate" @bind="selectedRefreshRate">
<select class="form-select" id="refreshRate" @bind="selectedRefreshRate" @bind:after="() => AfterRefreshRateChange()">
@foreach (var refreshRate in refreshRates)
{
<option value="@refreshRate.ToString()">@refreshRate.ToString() Hz</option>
@@ -182,6 +183,34 @@
</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.DpiScaling.ApplyScaling" id="applyDpiScaling">
}
<button class="accordion-button collapsed py-2 @(Preset.DpiScaling.ApplyScaling ? "text-warning fw-bold" : "")" type="button" data-bs-toggle="collapse" data-bs-target="#dpiScalingSettings">
Dpi Scaling
</button>
</span>
</h2>
<div id="dpiScalingSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<label class="form-label" for="dpiScaling">Percentage</label>
<select class="form-select" id="dpiScaling" @bind="Preset.DpiScaling.Percentage">
@foreach (var dpiScale in Shared.Native.CCD.DpiValues)
{
<option value="@dpiScale.ToString()">@dpiScale.ToString()</option>
}
</select>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<span class="d-flex align-items-baseline">
@@ -256,14 +285,8 @@
</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>
<RangeInput TValue="int" @bind-Value="Preset.ColorEnhancementSettings.DigitalVibranceLevel" Id="digitalVibrance" Label="Digital vibrance" />
<RangeInput TValue="int" @bind-Value="Preset.ColorEnhancementSettings.HueAngle" Id="hue" Label="Hue" Unit="°" Max="359" />
</div>
</div>
</div>
@@ -405,10 +428,8 @@
}
</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>
<NullableRangeInput TValue="int" @bind-Value="Preset.SDRBrightness" Id="sdrBrightness" Label="SDR brightness" Unit="%" />
</div>
</div>
</div>
@@ -539,11 +560,52 @@
</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">
Triggers
</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" NvPresetsEnabled="false" TPreset="NvPreset"></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>
</div>
}
</form>
</div>
<div class="modal-footer">
@if (Preset?.IsDisplayPreset == true)
{
<input class="form-check-input" type="checkbox" @bind="ApplyImmediately" id="directApply" @bind:after="ApplyImmediatelyAfter">
<label class="form-check-label" for="directApply">
Apply changes immediately
</label>
}
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="() => IsVisible = false">Close</button>
<button type="submit" class="btn btn-primary" disabled="@(!IsValid())" @onclick="ApplyClick">@(Preset?.IsDisplayPreset == true ? "Apply" : "Save")</button>
</div>
@@ -581,14 +643,33 @@
private bool IsGpuSettingsOpened;
private bool ShowOverclocking;
private bool ApplyImmediately;
private NvPreset? OriginalPreset;
private bool Applying;
private bool Queued;
protected override async Task OnParametersSetAsync()
{
if (!IsVisible)
{
return;
}
if ((IsDriverSettingsOpened || IsGpuSettingsOpened) && Preset != null)
{
return;
}
ApplyImmediately = false;
Preset = PresetParam == null ? null : PresetParam.CloneWithId();
displayPresets = await _rpcClientService.CallAsync<List<NvPreset>>("NvService", "GetDisplayPresets");
if (Preset?.IsDisplayPreset == true)
{
Preset = displayPresets.FirstOrDefault(p => p.DisplayId == Preset.DisplayId) ?? Preset;
}
if (Preset != null && PresetParam != null)
{
resolutions = await _rpcClientService.CallAsync<List<VirtualResolution>>("NvService", "GetAvailableResolutionsV2", Preset);
@@ -598,27 +679,51 @@
selectedVirtualResolution = Preset?.DisplayConfig.Resolution?.GetVirtualString();
selectedRefreshRate = Preset?.DisplayConfig.RefreshRate?.ToString();
if (Preset?.IsDisplayPreset == false)
{
displayPresets = await _rpcClientService.CallAsync<List<NvPreset>>("NvService", "GetDisplayPresets");
}
var deviceName = Preset?.IsDisplayPreset == true ? Preset.DisplayId : Preset?.DisplayId ?? displayPresets?.FirstOrDefault()?.DisplayId;
if (Preset != null)
{
ColorProfiles = await _rpcClientService.CallAsync<List<ColorProfileDto>>("ColorProfileService", "GetAllDisplayColorProfiles", deviceName);
OriginalPreset = Preset.Clone();
}
ApplyImmediately = await _localStorage.TryGet<bool>($"{nameof(NvPreset)}_{nameof(ApplyImmediately)}");
ShowOverclocking = true;
}
protected override void OnAfterRender(bool firstRender)
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// if (IsVisible && Preset?.IsDisplayPreset == false)
// {
// Preset.UpdateAutoApplySettings(keepChanges: true);
// }
await DoApplyImmediately();
}
private async Task DoApplyImmediately()
{
if (IsVisible && OriginalPreset != null && Preset?.IsDisplayPreset == true)
{
if (Applying)
{
Queued = true;
return;
}
do
{
Applying = true;
Queued = false;
Preset.UpdateAutoApplySettings(OriginalPreset, true);
if (ApplyImmediately && Preset.HasApplicableSettings)
{
await ApplyDisplayPreset();
}
OriginalPreset = Preset.Clone();
Applying = false;
}
while (Queued);
}
}
private async Task ApplyClick(MouseEventArgs e)
@@ -634,29 +739,6 @@
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)
@@ -734,4 +816,52 @@
{
return Preset?.IsDisplayPreset == true || !string.IsNullOrEmpty(Preset?.name);
}
private async Task AfterResolutionChange()
{
if (Preset == null)
{
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);
}
await DoApplyImmediately();
}
private async Task AfterRefreshRateChange()
{
if (Preset == null)
{
return;
}
var newRefreshRate = refreshRates?.FirstOrDefault(r => r.ToString() == selectedRefreshRate);
if (newRefreshRate != null)
{
Preset.DisplayConfig.RefreshRate = newRefreshRate;
}
await DoApplyImmediately();
}
private async Task ApplyImmediatelyAfter()
{
await _localStorage.SetAsync($"{nameof(NvPreset)}_{nameof(ApplyImmediately)}", ApplyImmediately);
}
}
@@ -19,6 +19,7 @@
<NvidiaDithering Visible="DitheringVisible" />
<NvidiaDriverSettings Preset="CurrentDriverSettingsPreset" />
<NvidiaGpuSettings Preset="CurrentGpuSettingsPreset" GpuInfo="CurrentGpuInfo" />
<NvidiaInfo Visible="InfoVisible" />
<div class="d-flex align-items-center">
<h1>NVIDIA controller</h1>
@@ -30,6 +31,7 @@
<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>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#infoModal" @onclick="() => InfoVisible = true">Info</a></li>
</ul>
</div>
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#settingsModal">
@@ -75,6 +77,7 @@ else
<div class="card-body">
<small class="text-body">@preset.DisplayConfig.Resolution.ToString() @@ @preset.DisplayConfig.RefreshRate.ToString()Hz</small>
<small class="text-muted">@preset.colorData</small>
<small class="text-muted">Scale: @preset.DpiScaling.ToString()</small>
@if (preset.DitherState != NvDitherState.Auto)
{
<br />
@@ -93,22 +96,7 @@ else
@if (preset.driverSettings?.Any() == true)
{
<br />
<div class="dropdown">
<span role="button" class="text-decoration-underline dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside" @onclick="() => DriverSettingsOnClick(preset)">
<small>@preset.driverSettings.Count() driver settings changed</small>
</span>
<form class="dropdown-menu">
@if (DriverSettings != null)
{
<ul class="list-group p-0">
@foreach (var setting in DriverSettings)
{
<li class="list-group-item py-0 px-1"><small>@setting.FriendlyName</small></li>
}
</ul>
}
</form>
</div>
@RenderPresetSettings(preset)
}
</div>
</div>
@@ -239,7 +227,7 @@ else
}
@if (preset.applyDriverSettings)
{
<li class="list-group-item p-1">@preset.driverSettings.Count changed driver settings</li>
<li class="list-group-item p-1">@RenderPresetSettings(preset)</li>
}
@if (preset.ApplyNovideoSettings)
{
@@ -278,6 +266,7 @@ else
private NvPreset? CurrentGpuSettingsPreset;
private NvGpuInfoDto? CurrentGpuInfo;
private bool DitheringVisible;
private bool InfoVisible;
private List<NvSettingItemDto>? DriverSettings;
private PresetOrder PresetOrder;
@@ -301,9 +290,42 @@ else
}
}
RenderFragment<NvPreset> RenderPresetSettings => preset =>
@<div class="dropdown">
<span role="button" class="text-decoration-underline dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside" @onclick="() => DriverSettingsOnClick(preset)" @onclick:preventDefault @onclick:stopPropagation>
<small>@preset.driverSettings.Count() driver settings changed</small>
</span>
<form class="dropdown-menu" style="width: max-content">
@if (DriverSettings != null)
{
<ul class="list-group p-0">
@foreach (var settingId in preset.driverSettings.Keys)
{
var setting = DriverSettings.FirstOrDefault(s => s.SettingId == settingId);
if (setting == null)
{
continue;
}
var settingValue = setting.Values.FirstOrDefault(v => v.Value == preset.driverSettings[settingId]);
if (settingValue == null) {
continue;
}
<li class="list-group-item py-0 px-1"><small>@setting.Name: @settingValue.ValueName</small></li>
}
</ul>
}
</form>
</div>
;
private async Task PresetOnClick(NvPreset preset)
{
await _rpcClientService.CallAsync<bool>("NvService", "ApplyPresetWithId", preset.id);
var result = await _rpcClientService.CallAsync<bool>("NvService", "ApplyPresetWithId", preset.id);
if (result)
{
DisplayPresets = await _rpcClientService.CallAsync<List<NvPreset>>("NvService", "GetDisplayPresets");
}
}
private void PresetOnEditClick(NvPreset preset)
@@ -395,11 +417,6 @@ else
private async Task DriverSettingsOnClick(NvPreset preset)
{
var driverSettings = await _rpcClientService.CallAsync<List<NvSettingItemDto>>("NvService", "GetDriverSettings", Type.Missing, true);
if (driverSettings != null)
{
DriverSettings = driverSettings.Where(s => s.Value != s.DefaultValue && s.State == 2).ToList();
}
DriverSettings ??= await _rpcClientService.CallAsync<List<NvSettingItemDto>>("NvService", "GetDriverSettings", Type.Missing, true);
}
}
@@ -134,13 +134,9 @@ else
<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>
<RangeInput TValue="int" Min="50" Max="10000" Step="50" @bind-Value="config.ProcessMonitorPollingInterval" Id="processMonitorPollingInterval" Label="Polling interval of process monitor" Unit=" milliseconds"
SubLabel="Decreasing this delay may execute triggered presets sooner but can cause a higher CPU load"
/>
<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">
@@ -0,0 +1,120 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.AMD
@using ColorControl.Shared.Contracts.RemoteControl
@using ColorControl.UI.Services
@using ColorControl.Shared.Common
@* @rendermode Constants.RenderMode *@
@typeparam TPreset where TPreset : PresetBase
@typeparam TButtonEnum where TButtonEnum : struct, Enum
@inject RpcUiClientService _rpcClientService
<div class="modal" id="rcButtonModal" data-bs-backdrop="false" tabindex="-1">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">RC button</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="() => Button = null"></button>
</div>
<div class="modal-body">
<form class="row g-1">
@if (Button == 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="Button.Name" required />
</div>
<div class="mb-2">
<label class="form-label" for="button">Button</label>
<select class="form-select" id="button" @bind="Button.Button">
@foreach (var buttonType in Enum.GetValues<TButtonEnum>())
{
<option value="@buttonType.ToString()">@buttonType.GetDescription()</option>
}
</select>
</div>
@if (Presets?.Any() == true)
{
<div class="mb-2">
<label class="form-label" for="preset">Preset</label>
<select class="form-select" id="preset" @bind="PresetName">
<option value="">Select preset</option>
@foreach (var preset in Presets)
{
<option value="@preset.name">@preset.name</option>
}
</select>
</div>
}
}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" @onclick="RemoveClick">Remove</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="() => Button = null">Close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" @onclick="ApplyClick">Save</button>
</div>
</div>
</div>
</div>
@code {
[Parameter]
public BaseRcButtonDto<TPreset, TButtonEnum>? ButtonParam { get; set; }
[Parameter]
public Func<BaseRcButtonDto<TPreset, TButtonEnum>, Task>? AfterApply { get; set; }
[Parameter]
public List<TPreset>? Presets { get; set; }
private BaseRcButtonDto<TPreset, TButtonEnum>? Button { get; set; }
private string? PresetName;
protected override void OnParametersSet()
{
if (ButtonParam == null)
{
return;
}
Button = new BaseRcButtonDto<TPreset, TButtonEnum>(ButtonParam);
PresetName = Button.Preset?.name;
}
private async Task ApplyClick(MouseEventArgs e)
{
if (Button == null || ButtonParam == null)
{
return;
}
Button.Preset = (Presets?.FirstOrDefault(p => p.name == PresetName))!;
ButtonParam.Update(Button);
if (AfterApply != null)
{
await AfterApply.Invoke(ButtonParam);
}
}
private async Task RemoveClick(MouseEventArgs e)
{
if (Button == null || ButtonParam == null)
{
return;
}
if (AfterApply != null)
{
await AfterApply.Invoke(null!);
}
}
}
@@ -0,0 +1,319 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.Base
@using ColorControl.Shared.Contracts.RemoteControl
@using ColorControl.UI.Services
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common
@using System.Globalization
@* @rendermode Constants.RenderMode *@
@typeparam TDevice where TDevice : BaseDeviceDto
@typeparam TPreset where TPreset : PresetBase, new()
@typeparam TButtonEnum where TButtonEnum : struct, Enum
@inject RpcUiClientService _rpcClientService
@inject JSHelper jsHelper
<style>
.myhover {
border-radius: 25px;
width: @(ButtonSize)%;
aspect-ratio: 1;
opacity: 0.2;
border-width: 0;
}
.myhover:hover {
opacity: 0.4;
background-color: white;
}
</style>
<GenericRcButtonPage ButtonParam="CurrentButton" AfterApply="AfterApplyButton" TPreset="TPreset" TButtonEnum="TButtonEnum" Presets="Presets"></GenericRcButtonPage>
<div>
<div>
<h5>@(Device?.Name == null ? $"No device selected" : Device.Name)</h5>
</div>
<div>
<form class="row g-1 needs-validation @WasValidated" novalidate>
@if (Remotes == null)
{
<div>Loading...</div>
}
else
{
<select class="form-select" id="remoteControl" @bind="SelectedRemoteName" @bind:after="RemoteChanged">
@foreach (var remote in Remotes)
{
<option id="@remote.Name">@remote.Name</option>
}
</select>
if (RemoteController == null)
{
<div>Select a remote</div>
}
else
{
@if (AllowEditing)
{
<button type="button" class="btn btn-primary" data-bs-dismiss="@(IsValid() ? "modal" : "")" @onclick="ApplyClick">Save</button>
}
@* <p>Left: @CurrentX, Top: @CurrentY</p> *@
<div style="position: relative" @onclick="Button_Clicked" @oncontextmenu:preventDefault="true" @oncontextmenu="e => NewRcButtonRightClick(e)">
<img src="remotes/@RemoteController.ImageFileName" id="rcImage" class="w-100" />
@foreach (var button in RemoteController.Buttons)
{
<button class="myhover" type="button"
style="position: absolute; top: @(button.Top.ToString(CultureInfo.InvariantCulture))%; left: @(button.Left.ToString(CultureInfo.InvariantCulture))%"
@onclick="() => RcButtonOnClick(button)" @oncontextmenu="e => RcButtonRightClick(e, button)" @oncontextmenu:preventDefault="true" @oncontextmenu:stopPropagation>
</button>
}
</div>
}
}
</form>
</div>
</div>
@code {
private TDevice? _device;
[Parameter]
public TDevice? Device { get; set; }
[Parameter]
public Func<TDevice, Task>? AfterApply { get; set; }
[Parameter]
public string? ServiceName { get; set; }
[Parameter]
public List<TPreset>? Presets { get; set; }
private string WasValidated = "";
private bool IsVisible;
private string? SelectedRemoteName;
private BaseRcDto<TPreset, TButtonEnum>? RemoteController;
private List<BaseRcDto<TPreset, TButtonEnum>>? Remotes;
private double CurrentX, CurrentY;
private RectDimension? ImageDimension;
private BaseRcButtonDto<TPreset, TButtonEnum>? CurrentButton;
private double ButtonSize = 13;
private bool AllowEditing = false;
protected override async void OnParametersSet()
{
if (ServiceName != null && (_device != Device || Device == null))
{
_device = Device;
Remotes = await _rpcClientService.CallAsync<List<BaseRcDto<TPreset, TButtonEnum>>>(ServiceName, "GetRemotes");
if (Remotes?.Any() == true)
{
RemoteController = Remotes.First();
SelectedRemoteName = RemoteController.Name;
StateHasChanged();
}
}
}
private async Task ApplyClick(MouseEventArgs e)
{
if (RemoteController == null || ServiceName == null)
{
return;
}
if (!IsValid())
{
WasValidated = "was-validated";
return;
}
var result = await _rpcClientService.CallAsync<bool>(ServiceName, "UpdateRemote", RemoteController);
// if (AfterApply != null)
// {
// await AfterApply.Invoke(Device);
// }
}
private void ModalOnFocus(FocusEventArgs e)
{
if (!IsVisible)
{
WasValidated = "";
IsVisible = true;
}
}
private bool IsValid()
{
return true; // !string.IsNullOrEmpty(Device?.Name) && (Device?.IsCustom != true || !string.IsNullOrEmpty(Device?.IpAddress));
}
private async Task RcButtonOnClick(BaseRcButtonDto<TPreset, TButtonEnum> button)
{
if (ServiceName == null)
{
return;
}
var rcButton = button.Button;
var preset = button.Preset;
if (preset == null)
{
preset = new TPreset();
preset.name = rcButton.ToString();
preset.SetSteps([rcButton.ToString()]);
}
await _rpcClientService.CallAsync<bool>(ServiceName, "ApplyPreset", preset);
}
public async Task Button_Clicked(MouseEventArgs e)
{
if (ImageDimension == null)
{
ImageDimension = await jsHelper.GetElementDimensions("rcImage");
}
if (ImageDimension != null)
{
CurrentX = Math.Round(((double)e.OffsetX / ImageDimension.Width) * 100, 4);
CurrentY = Math.Round(((double)e.OffsetY / ImageDimension.Height) * 100, 4);
return;
}
CurrentX = e.OffsetX;
CurrentY = e.OffsetY;
}
private async Task RcButtonRightClick(MouseEventArgs e, BaseRcButtonDto<TPreset, TButtonEnum> button)
{
if (!AllowEditing)
{
return;
}
CurrentButton = button;
if (e.ShiftKey)
{
AlignItemsHorizontally(button);
return;
}
if (e.CtrlKey)
{
AlignItemsVertically(button);
return;
}
await jsHelper.OpenModal("rcButtonModal");
}
private void AlignItemsHorizontally(BaseRcButtonDto<TPreset, TButtonEnum> button)
{
if (RemoteController?.Buttons == null)
{
return;
}
var alignedButtons = RemoteController.Buttons.Where(b => b.Top - 2 <= button.Top && b.Top + 2 >= button.Top);
foreach (var b in alignedButtons)
{
b.Top = button.Top;
}
}
private void AlignItemsVertically(BaseRcButtonDto<TPreset, TButtonEnum> button)
{
if (RemoteController?.Buttons == null)
{
return;
}
var alignedButtons = RemoteController.Buttons.Where(b => b.Top - 15 <= button.Top && b.Top + 15 >= button.Top && b.Left - 8 <= button.Left && b.Left + 8 >= button.Left);
foreach (var b in alignedButtons)
{
b.Left = button.Left;
}
}
private async Task NewRcButtonRightClick(MouseEventArgs e)
{
if (!AllowEditing)
{
return;
}
if (ImageDimension == null)
{
ImageDimension = await jsHelper.GetElementDimensions("rcImage");
}
if (ImageDimension != null)
{
var posX = Math.Round(((double)e.OffsetX / ImageDimension.Width) * 100, 4);
var posY = Math.Round(((double)e.OffsetY / ImageDimension.Height) * 100, 4);
posX -= (ButtonSize / 2);
posY -= ((double)ImageDimension.Width / ImageDimension.Height) * ((ButtonSize / 2) - 0.5);
CurrentButton = new BaseRcButtonDto<TPreset, TButtonEnum>
{
Top = posY,
Left = posX
};
await jsHelper.OpenModal("rcButtonModal");
}
}
private Task AfterApplyButton(BaseRcButtonDto<TPreset, TButtonEnum>? button)
{
if (RemoteController == null)
{
return Task.CompletedTask;
}
if (button == null)
{
RemoteController.Buttons.Remove(CurrentButton);
}
else if (!RemoteController.Buttons.Contains(button))
{
RemoteController.Buttons.Add(button);
}
StateHasChanged();
return Task.CompletedTask;
}
private void KeyDown(KeyboardEventArgs e)
{
}
private void KeyUp(KeyboardEventArgs e)
{
}
private void RemoteChanged()
{
RemoteController = Remotes?.FirstOrDefault(r => r.Name == SelectedRemoteName);
ImageDimension = null;
}
}
@@ -1,5 +1,6 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Services
@using NvAPIWrapper.Native.Display
@using ColorControl.Shared.Common
@@ -7,10 +8,8 @@
@rendermode Constants.RenderMode
@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" id="samsungDeviceModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" @onfocus="ModalOnFocus">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
@@ -73,7 +72,7 @@
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#deviceOptions">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#deviceOptions">
Device options
</button>
</h2>
@@ -165,6 +164,10 @@
</form>
</div>
<div class="modal-footer">
@if (Device?.IsCustom == true)
{
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#removeDeviceModal">Remove</button>
}
<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>
@@ -172,6 +175,8 @@
</div>
</div>
<ConfirmModal ModalId="removeDeviceModal" Title="Remove device" Message="Are you sure you want to remove this device?" OnConfirm="DeviceOnRemoveConfirmed" NextModalId="samsungDeviceModal" />
@code {
private SamsungDeviceDto? _device;
@@ -216,6 +221,21 @@
}
}
private async Task DeviceOnRemoveConfirmed()
{
if (Device == null)
{
return;
}
var result = await _rpcClientService.CallAsync<bool>("SamsungService", "RemoveDevice", Device);
if (AfterApply != null)
{
await AfterApply.Invoke(Device);
}
}
private async Task TestConnectionClick(MouseEventArgs e)
{
IsTestingConnection = true;
@@ -9,8 +9,6 @@
@rendermode Constants.RenderMode
@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-scrollable modal-dialog-centered">
@@ -4,6 +4,7 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.UI.Components.Pages.RemoteControl
@using ColorControl.UI.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Generics
@@ -22,6 +23,9 @@
<div class="d-flex align-items-center">
<h1>Samsung controller</h1>
<div class="d-flex ms-auto">
<button type="button" class="btn btn-sm btn-primary me-2" data-bs-toggle="offcanvas" data-bs-target="#rcCanvasRight" aria-controls="rcCanvasRight">
Remote control
</button>
<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
@@ -62,6 +66,16 @@
</div>
</div>
<div class="offcanvas offcanvas-end" style="width: 280px" data-bs-scroll="true" tabindex="-1" id="rcCanvasRight" aria-labelledby="rcCanvasRightRightLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="rcCanvasRightRightLabel">Remote control</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<GenericRcPage ServiceName="SamsungService" Device="CurrentDevice" TDevice="SamsungDeviceDto" TPreset="SamsungPreset" TButtonEnum="ButtonType"></GenericRcPage>
</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" />
@@ -82,7 +96,7 @@ else
}
<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()">
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#samsungDeviceModal" @onclick="() => AddDeviceOnClick()">
Add
</button>
</div>
@@ -117,7 +131,7 @@ else
{
<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)">
<button type="button" class="btn btn-sm btn-primary ms-2" data-bs-toggle="modal" data-bs-target="#samsungDeviceModal" @onclick="() => DeviceOnClick(device)">
Settings
</button>
</div>
@@ -210,6 +224,7 @@ else
Config = await _rpcClientService.CallAsync<SamsungServiceConfigDto>("SamsungService", "GetConfig");
await RefreshPresets();
Devices = await _rpcClientService.CallAsync<List<SamsungDeviceDto>>("SamsungService", "GetDevices");
CurrentDevice = Devices?.FirstOrDefault(d => d.MacAddress == Config?.PreferredMacAddress) ?? Devices?.FirstOrDefault();
InvokableActions = await _rpcClientService.CallAsync<List<InvokableActionDto<SamsungPreset>>>("SamsungService", "GetInvokableActions");
}