Compare commits

...
36 Commits
Author SHA1 Message Date
Maassoft d0d3bb4682 upgrade to .NET, fix webOS 26 compatibility, run new UI in separate process 2026-05-02 17:39:33 +02:00
月鉴风华andcyc19950621 8cb427f0b0 fix: update LG webOS register payload for webOS 26 compatibility (#577)
Co-authored-by: cyc19950621 <cyc19950621@users.noreply.github.com>

I will add a toggle so users have the option to activate/deactivate the new handshake (in case the new one does not work or reduces functionality on older webOS).
2026-05-02 14:07:29 +02:00
Maassoft 1c22b2daf8 fix some issues, add ddc settings, make new ui default 2025-09-28 15:10:25 +02:00
Maassoft 827157f30f fix several issues, add remote controls to new UI 2025-05-30 17:20:37 +02:00
DemoJameson c6c7e1ccc0 Add some LG settings from new devices (#502)
Includes 'Quick Frame Transport', '144Hz/165Hz Mode', 'ALLM', 'Dolby Vision PC'
2025-05-26 19:06:35 +02:00
Victor Nicolás Lizarraga Ochoa 043e5e7ff2 Add piecewise tone mapping option (#454)
* Add piecewise tone mapping option

* Add curve like to tone mapping transfer function

* Fix conflicts

* Experiments

* Add piecewise tone mapped option to UI

* Fix IIcc context diff and reset cproj settings
2025-03-29 17:27:01 +01:00
Vincent Maas 6e206a6728 fix Nvidia crash, remove UWP dependencies 2025-01-26 10:38:56 +01:00
Myungchul Keum d31d2f321f Add LG Dark room Mode (#459) 2024-12-28 11:48:11 +01:00
Vincent Maas 5c62273b33 fix color profile issues, add lock/unlock power off/on, add GPU OC settings to new UI 2024-10-20 11:18:02 +02:00
Vincent Maas 6f4b1e9779 fix condition 2024-09-22 15:52:42 +02:00
Vincent Maas c2b220c70b Merge branch 'master' of https://github.com/Maassoft/ColorControl 2024-09-22 15:27:16 +02:00
André Silva de Jesus a236e7a8a5 Support for display change trigger, connected display name preset condition and minor changes (#412)
* add displaychange trigger, connected display filter and other minor changes

* Undo unintended designer changes on LgPanel

* Try to fix unintended changes in the samsung panel

* Fix async handling of window message dispatcher event; Change to use native method for display enum as Screen.AllScreen caches and returns wrong results

* Uncomment SignManifests

* Fix mixed tabs/spaces
2024-09-22 15:27:07 +02:00
Vincent Maas 899e49c2d9 fix some Nvidia issues, improve color profiles, add GPU info to new UI 2024-09-22 15:24:54 +02:00
Vincent Maas f42a46e53f fix issues with new UI, add color profiles, add sdr brightness for AMD 2024-09-01 16:24:05 +02:00
Maassoft 4d2ca8a552 Merge pull request #392 from BergHeisen/improve-wol
Improve Wake on Lan Code
2024-08-11 17:22:36 +02:00
Marlon 42f6b3346c Filter on Description rather than name to detect a Hyper-V Interface 2024-08-04 20:17:27 +02:00
Marlon 048abda482 Explicitly bind to IP Address for the UDP Client 2024-08-04 19:44:45 +02:00
Marlon 05c938c8ac Correctly filter out Hyper-V Interfaces 2024-08-04 19:43:43 +02:00
Marlon 1347b700f9 Improve Wake on Lan Code 2024-08-04 16:33:23 +02:00
Vincent Maas 9e8e77d89b fix some issues after new release 2024-08-03 16:19:27 +02:00
Vincent Maas 18f32e1d46 fix some final issues 2024-08-02 14:52:25 +02:00
Vincent Maas 4ed670c01b add pages for new UI, fix issues 2024-08-02 13:47:03 +02:00
Vincent Maas 9a9424f1af fix some issues and add test Blazor frontend 2024-05-10 13:35:35 +02:00
Vincent Maas 27bc0505b7 fix shutdown-event, add disable error popup setting, use better display id for Nvidia, fix refresh rate issues and improve background color 2024-04-21 11:48:04 +02:00
Vincent Maas 4f4ce54ade fix amd/nvidia refresh rate and resolution issues 2024-04-14 17:12:03 +02:00
Vincent Maas b9db10262b before refreshrate overhaul 2024-04-06 13:29:02 +02:00
Vincent Maas b8d64c7322 Merge branch 'master' of https://github.com/Maassoft/ColorControl 2024-04-02 21:49:05 +02:00
Vincent Maas fbd25f46b7 remove elevated form and app context provider 2024-04-02 21:49:02 +02:00
Maassoft 0c86466d7f Merge pull request #349 from WildFireFlum/feature/add-cs3-full-service-menu
Support enabling full service menu on LG CS3VA
2024-04-02 19:49:14 +02:00
Ynon Flum fd5a2aa7fe Add option to enable full service menu on LG CS3VA 2024-04-02 00:29:28 +03:00
Vincent Maas bc162af76e remove all old logic from main form and fix issues 2024-03-23 17:38:37 +01:00
Vincent Maas dd3a996172 use worker to start services and remove startup code from main form 2024-03-16 13:22:27 +01:00
Vincent Maas ab2ca92827 fix scaling issue after .NET 8 update 2024-03-12 22:35:46 +01:00
Vincent Maas f08dcb7661 update to .NET 8, fix conditions, add Nvidia RTX settings, add LG actions, improve Game Bar 2024-03-10 17:07:55 +01:00
Vincent Maas b1120c8d49 add creation of SDR color profile, add NVIDIA HDR10+ support, fix MinTML/MaxTML, add LG TPC/GSR actions, add LG/Samsung trigger events 2024-02-11 14:38:53 +01:00
Vincent Maas c958e6cb04 remove Nvapi dependency when reading EDID, replace debug version of lcms2.dll with release version 2023-12-29 14:20:14 +01:00
313 changed files with 35559 additions and 13395 deletions

No files matched your search

+165
View File
@@ -0,0 +1,165 @@
using ColorControl.Shared.Common;
using ColorControl.Shared.Contracts;
using ColorControl.UI.Pages.Generic;
using ColorControl.UI.Services;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using NStandard;
using System.Net;
namespace ColorControl.UI;
public static class Blazor
{
private static WebApplication? CurrentApplication;
public static CustomComponentBase? CurrentComponent { get; private set; }
public static async Task Start(string[] args)
{
var mutexId = args.GetValueOrDefault(0);
var useDarkMode = args.GetValueOrDefault(1) == "True";
var uiPort = Utils.ParseInt(args.GetValueOrDefault(2));
var allowRemoteConnections = args.GetValueOrDefault(3) == "True";
await Stop();
var builder = WebApplication.CreateBuilder();
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddSingleton(new AppState { SelectedTheme = useDarkMode ? "dark" : "light" });
builder.Services.AddTransient<RpcUiClientService>();
builder.Services.AddTransient<JSHelper>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<NotificationService>();
builder.Services.Configure<HostOptions>(opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(3));
var listenPort = uiPort > 0 ? uiPort : 0;
builder.WebHost.ConfigureKestrel(options =>
{
if (allowRemoteConnections)
{
options.ListenAnyIP(listenPort);
}
else
{
options.Listen(IPAddress.Parse("127.0.0.1"), listenPort);
}
});
builder.WebHost.UseStaticWebAssets();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
//app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets();
//app.UsePathBase("/");
app.MapRazorComponents<Components.App>()
.AddInteractiveServerRenderMode();
CurrentApplication = app;
var _ = Task.Run(async () =>
{
await Task.Delay(500);
var port = GetCurrentPort();
if (port == -1)
{
return;
}
var rpcServer = app.Services.GetRequiredService<RpcUiClientService>();
await rpcServer.CallAsync<bool>("OptionsService", "SetCurrentUiSettings", port, allowRemoteConnections);
if (!mutexId.IsNullOrWhiteSpace())
{
Console.WriteLine("Waiting for mutex: " + mutexId);
var mutex = new Mutex(false, mutexId, out var createdNew);
if (createdNew)
{
Console.WriteLine("Mutex was created, skipping.");
return;
}
try
{
mutex.WaitOne();
Console.WriteLine("Waiting done.");
}
catch (Exception ex)
{
Console.WriteLine("Wait completed: " + ex.Message);
}
Environment.Exit(0);
}
});
await app.RunAsync();
}
public static async Task Stop()
{
if (CurrentApplication != null)
{
await CurrentApplication.StopAsync();
CurrentApplication = null;
}
}
public static int GetCurrentPort(Config? config = null)
{
if (CurrentApplication == null)
{
return -1;
}
var server = CurrentApplication.Services.GetService<IServer>();
var addressFeature = server?.Features.Get<IServerAddressesFeature>();
if (addressFeature == null)
{
return -1;
}
foreach (var address in addressFeature.Addresses)
{
return int.Parse(address.Split(':').Last());
}
return -1;
}
public static void SetCurrentComponent(CustomComponentBase? customComponentBase)
{
CurrentComponent = customComponentBase;
}
public static void ClearCurrentComponent(CustomComponentBase customComponentBase)
{
if (CurrentComponent == customComponentBase)
{
CurrentComponent = null;
}
}
}
+92
View File
@@ -0,0 +1,92 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NLog" Version="6.1.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Shared\Shared.csproj" />
<ProjectReference Include="..\MHC2Gen\MHC2Gen.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="Components\Pages\ColorProfile\ColorProfilePage.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Generic\FieldDefs.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Generic\ConfirmModal.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Game\GameSummary.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Generic\NotificationsComponent.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Generic\TriggerInput.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Info.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\LG\LgExecuteActionModal.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Nvidia\NvidiaGpuSettings.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Samsung\SamsungSummary.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Nvidia\NvidiaSettings.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Nvidia\NvidiaPreset.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Samsung\SamsungDevicePage.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Generic\StepsInput.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Samsung\SamsungServiceMenu.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="wwwroot\app.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\favicon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\helper.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\remotes\LG_FactorySVC.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\remotes\LG_MR20GA.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\remotes\LG_MR23GN.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\remotes\Samsung_AA81-00243A.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\remotes\Samsung_BN59-01315B.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
+80
View File
@@ -0,0 +1,80 @@
@using ColorControl.Shared.Common
@using ColorControl.Shared.Contracts
@using ColorControl.UI.Pages.Generic
@using ColorControl.UI.Services
@using ColorControl.UI.Components.Pages.Generic
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject AppState AppState
@inject RpcUiClientService _rpcClientService
@inject NavigationManager _navigationManager
@inject NotificationService _notificationService
@inject ProtectedSessionStorage _sessionStorage
@inject JSHelper _jsHelper
@implements IDisposable
@attribute [StreamRendering]
<!DOCTYPE html>
<html lang="en" data-bs-theme="@AppState.SelectedTheme">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<ResourcePreloader />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="ColorControl.UI.styles.css" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="InteractiveServer" />
<script>
addEventListener('focus', (event) => {
var isModalShown = document.getElementsByClassName("modal show").length > 0;
DotNet.invokeMethodAsync("ColorControl.UI", "RefreshSummary", isModalShown);
});
</script>
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<script src="@Assets["_framework/blazor.web.js"]"></script>
<NotificationsComponent></NotificationsComponent>
</body>
</html>
@code {
private Config? Config;
protected override async Task OnInitializedAsync()
{
Config ??= await _rpcClientService.CallAsync<Config>("OptionsService", "GetConfig");
var newTheme = Config?.UseDarkMode == true ? "dark" : "light";
if (newTheme != AppState.SelectedTheme)
{
AppState.SelectedTheme = Config?.UseDarkMode == true ? "dark" : "light";
_navigationManager.Refresh();
}
AppState.OnChange += StateHasChanged;
}
public void Dispose()
{
AppState.OnChange -= StateHasChanged;
}
[JSInvokable]
public static async Task RefreshSummary(bool isModalShown)
{
if (Blazor.CurrentComponent is SummaryComponentBase summaryComponent && !isModalShown)
{
await summaryComponent.RefreshExternal();
}
}
}
@@ -0,0 +1,21 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<TopMenu />
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
@@ -0,0 +1,96 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(0, 40, 90) 0%, #101047 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
@@ -0,0 +1,71 @@
@using ColorControl.Shared.Contracts
@using ColorControl.UI.Services;
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">ColorControl Web App</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link px-3" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
@if (modules != null)
{
@foreach (var module in modules.Where(m => m.IsActive))
{
<div class="nav-item px-3">
<NavLink class="nav-link px-3 align-items-baseline" href="@module.Link">
<span class="bi bi-@module.GetIconClass()" aria-hidden="true"></span> @module.DisplayName
</NavLink>
</div>
}
}
<div class="nav-item px-3">
<NavLink class="nav-link px-3 align-items-baseline" href="options">
<span class="bi bi-gear-fill" aria-hidden="true"></span> Options
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link px-3 align-items-baseline" href="ColorProfile">
<span class="bi bi-palette-fill" aria-hidden="true"></span> Color Profiles
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link px-3 align-items-baseline" href="logging">
<span class="bi bi-list-columns-reverse" aria-hidden="true"></span> Logging
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link px-3 align-items-baseline" href="info">
<span class="bi bi-info-circle-fill" aria-hidden="true"></span> Info
</NavLink>
</div>
</nav>
</div>
@code {
private Config? config;
private List<ModuleDto>? modules;
protected override async Task OnInitializedAsync()
{
config = await _rpcClientService.CallAsync<Config>("OptionsService", "GetConfig");
modules = config?.Modules?.Select(m => new ModuleDto { DisplayName = m.DisplayName, IsActive = m.IsActive, Link = m.DisplayName.Split(' ')[0] }).ToList();
}
}
@@ -0,0 +1,109 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.bi-list-logging-nav-menu {
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="white" class="bi bi-card-text" viewBox="0 0 16 16"><path d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2z"/><path d="M3 5.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5M3 8a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9A.5.5 0 0 1 3 8m0 2.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5"/></svg>');
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}
@@ -0,0 +1,29 @@
@using ColorControl.Shared.Contracts
@using ColorControl.UI.Services
@inherits LayoutComponentBase
@inject AppState AppState
@inject NavigationManager _navigationManager;
@rendermode Constants.RenderMode
<div class="top-row px-4 pt-3 text-end">
@* <button type="button" class="btn btn-primary me-2" @onclick="ChangeTheme">@ThemeTransitionName</button> *@
<a href="https://github.com/Maassoft/ColorControl" target="_blank">GitHub</a>
</div>
@code {
private string ThemeTransitionName = "Dark";
protected override void OnInitialized()
{
ThemeTransitionName = AppState.SelectedTheme == "light" ? "Dark theme" : "Light theme";
}
private void ChangeTheme()
{
AppState.SetTheme(AppState.SelectedTheme == "light" ? "dark" : "light");
ThemeTransitionName = AppState.SelectedTheme == "light" ? "Dark theme" : "Light theme";
_navigationManager.Refresh();
}
}
@@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}
@@ -0,0 +1,388 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.AMD
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.Shared.Common;
@using ColorControl.UI.Services
@rendermode Constants.RenderMode
@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-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(Preset?.IsDisplayPreset == true ? $"Settings for display {Preset?.displayName}" : Preset?.id == 0 ? $"Settings for new preset" : $"Settings of preset {Preset?.IdOrName}")</h5>
<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">
@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" 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>
}
<NullableRangeInput TValue="int" @bind-Value="Preset.SDRBrightness" Id="sdrBrightness" Label="SDR brightness" Unit="%" />
</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 == false)
{
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.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
<div class="modal" id="settingsModal" tabindex="-1" @onfocus="OnFocus">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">AMD settings</h5>
<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,234 @@
@page "/AMD"
@using ColorControl.Shared.Common
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.AMD
@using ColorControl.UI.Pages.Generic
@using ColorControl.UI.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inherits SummaryComponentBase
@rendermode Constants.RenderMode
@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">
@if (!DisplayPresets.Any())
{
<span>No displays found</span>
}
@foreach (var preset in DisplayPresets)
{
<div class="col-lg-6 mb-3">
<div class="card h-100">
<div class="card-header d-flex align-items-baseline">
<span>
@preset.displayName
@if (preset.DisplayConfig.IsPrimary == true)
{
<small class="text-muted ms-2">Primary</small>
}
</span>
<div class="d-flex ms-auto">
<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>
<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">
@if (!Presets.Any())
{
<span>No presets found</span>
}
<div class="list-group">
@foreach (var preset in Presets)
{
<span class="list-group-item list-group-item-action" @onclick="() => PresetOnClick(preset)" @onclick:preventDefault @onclick:stopPropagation>
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="mb-1">
@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 class="d-none d-lg-block">
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Delete</button>
<button class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Clone</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Edit</button>
</span>
<div class="d-lg-none" @onclick:preventDefault @onclick:stopPropagation>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)">Delete</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)">Clone</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)">Edit</a></li>
</ul>
</div>
</div>
<p class="mb-1">@preset.DisplayConfig.Resolution.ToString() @@ @preset.DisplayConfig.RefreshRate.ToString()Hz @(preset.applyColorData ? preset.colorDepth : "")</p>
<small class="text-muted">@preset.shortcut</small>
</span>
}
</div>
</div>
</div>
}
@code {
private List<AmdPreset>? Presets;
private List<AmdPreset>? DisplayPresets;
private AmdPreset? CurrentPreset;
private PresetOrder PresetOrder;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await Refresh();
}
protected override async Task Refresh()
{
await RefreshPresets();
DisplayPresets = await _rpcClientService.CallAsync<List<AmdPreset>>("AmdService", "GetDisplayPresets");
}
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 void PresetOnEditClick(AmdPreset preset)
{
CurrentPreset = preset;
}
private void PresetOnCloneClick(AmdPreset preset)
{
CurrentPreset = new AmdPreset(preset);
CurrentPreset.name = preset.name;
CurrentPreset.SetCloneValues();
}
private void 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 void DisplayPresetOnClick(AmdPreset preset)
{
CurrentPreset = preset;
}
private void 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);
}
}
@@ -0,0 +1,533 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.AMD
@using ColorControl.Shared.Contracts.DisplayInfo
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.Shared.Common;
@using ColorControl.UI.Services
@using MHC2Gen
@using Microsoft.JSInterop;
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
@inject JSHelper jsHelper;
<div class="modal modal-lg" id="colorProfileModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" @onfocus="ModalOnFocus">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(ColorProfile?.Name == null ? $"Settings for new color profile" : $"Settings of color profile {ColorProfile.Name}")</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="() => IsVisible = false"></button>
</div>
<div class="modal-body">
@if (ColorProfile?.HasExtraInfo == false)
{
<div class="alert alert-warning" role="alert">
This is not a profile created by ColorControl. Some fields are set to default.
</div>
}
@if (!string.IsNullOrEmpty(_rpcClientService.LastErrorMessage))
{
<div class="alert alert-danger" role="alert">
Error: @_rpcClientService.LastErrorMessage
</div>
}
<form class="row g-1 needs-validation @WasValidated" id="profileForm" novalidate>
@if (ColorProfile == null || DisplayInfo == null || CurrentColorInfo == null)
{
<div>Loading...</div>
}
else
{
<div class="mb-2">
<label class="form-label" for="display">Display</label>
<input class="form-control" type="text" id="display" value="@DisplayInfo" disabled />
</div>
<div class="mb-2">
<label class="form-label" for="name">Name</label>
<input class="form-control" type="text" id="name" @bind="ColorProfile.Name" @bind:after="AfterNameChanged" required />
</div>
<div class="mb-2">
<label class="form-label" for="description">Description</label>
<input class="form-control" type="text" id="description" @bind="ColorProfile.Description" />
</div>
<div class="mb-2">
<label class="form-label" for="renderingIntent">Rendering intent</label>
<select class="form-select" id="renderingIntent" @bind="ColorProfile.RenderingIntent">
@foreach (var renderingIntent in Enum.GetValues<LittleCms.RenderingIntent>().Where(ri => ri <= LittleCms.RenderingIntent.ABSOLUTE_COLORIMETRIC))
{
<option value="@renderingIntent">@renderingIntent.GetDescription()</option>
}
</select>
</div>
<div class="accordion accordion-flush" id="presetAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<span class="d-flex align-items-baseline">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#displaySettings">
Color gamut
</button>
</span>
</h2>
<div id="displaySettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="mb-2">
<div class="row">
<div class="col">
<label class="form-label" for="primariesSource"><small>Show Display Primaries and Luminance values of</small></label>
<select class="form-select" id="primariesSource" @bind="DisplayPrimariesSource" @bind:after="DisplayPrimariesSourceChanged">
@foreach (var colorInfo in DisplayColors)
{
<option value="@colorInfo.DisplayPrimariesSource">@(colorInfo.CustomName ?? colorInfo.DisplayPrimariesSource.ToString())</option>
}
</select>
</div>
<div class="col align-content-end">
<button class="btn btn-primary" type="button" disabled="@(DisplayPrimariesSource == DisplayPrimariesSource.ColorProfile)" @onclick="OverwritePrimariesOnClick">Overwrite color profile values</button>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label text-danger" for="redX">Red X-coordinate</label>
<input class="form-control" id="redX" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.Red.X" disabled="@PrimariesDisabled" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label text-danger" for="redY">Red Y-coordinate</label>
<input class="form-control" id="redY" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.Red.Y" disabled="@PrimariesDisabled" />
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label text-success" for="greenX">Green X-coordinate</label>
<input class="form-control" id="greenX" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.Green.X" disabled="@PrimariesDisabled" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label text-success" for="greenY">Green Y-coordinate</label>
<input class="form-control" id="greenY" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.Green.Y" disabled="@PrimariesDisabled" />
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label text-primary" for="blueX">Blue X-coordinate</label>
<input class="form-control" id="blueX" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.Blue.X" disabled="@PrimariesDisabled" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label text-primary" for="blueY">Blue Y-coordinate</label>
<input class="form-control" id="blueY" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.Blue.Y" disabled="@PrimariesDisabled" />
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="whiteX">White X-coordinate</label>
<input class="form-control" id="whiteX" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.White.X" disabled="@PrimariesDisabled" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="whiteY">White Y-coordinate</label>
<input class="form-control" id="whiteY" type="number" min="0" max="1" step="0.000000000000001" @bind="CurrentColorInfo.RgbPrimaries.White.Y" disabled="@PrimariesDisabled" />
</div>
</div>
</div>
<div class="mb-2">
<label class="form-label" for="targetGamut">Target gamut</label>
<select class="form-select" id="targetGamut" @bind="ColorProfile.ColorGamut">
@foreach (var gamut in Enum.GetValues<ColorGamut>())
{
<option value="@gamut">@gamut.GetDescription()</option>
}
</select>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<span class="d-flex align-items-baseline">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#luminanceSettings">
Luminance settings
</button>
</span>
</h2>
<div id="luminanceSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="minLuminance">Minimum luminance (nits)</label>
<input class="form-control" id="minLuminance" type="number" min="0" max="10" @bind="CurrentColorInfo.BlackLuminance" disabled="@PrimariesDisabled" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="maxLuminance">Max full frame luminance (nits)</label>
<input class="form-control" id="maxLuminance" type="number" min="0" max="10000" @bind="CurrentColorInfo.WhiteLuminance" disabled="@PrimariesDisabled" />
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="minCLL">Minimum brightness (MinCLL) (nits)</label>
<input class="form-control" id="minCLL" type="number" min="0" max="10" @bind="CurrentColorInfo.MinCLL" disabled="@PrimariesDisabled" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="maxCLL">Peak brightness (MaxCLL) (nits)</label>
<input class="form-control" id="maxCLL" type="number" min="0" max="10000" @bind="CurrentColorInfo.MaxCLL" disabled="@PrimariesDisabled" />
</div>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<span class="d-flex align-items-baseline">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#hdrSettings">
Advanced EOTF settings
</button>
</span>
</h2>
<div id="hdrSettings" class="accordion-collapse collapse" data-bs-parent="#presetAccordion">
<div class="accordion-body">
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="sdrTransfer">SDR Transfer Function</label>
<select class="form-select" id="sdrTransfer" @bind="ColorProfile.SDRTransferFunction">
@foreach (var function in Enum.GetValues<SDRTransferFunction>())
{
<option value="@function">@function.GetDescription()</option>
}
</select>
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="gamma">Gamma</label>
<input class="form-control" id="gamma" type="number" min="0.1" max="10" step="0.01" @bind="ColorProfile.Gamma" disabled="@(ColorProfile.SDRTransferFunction != SDRTransferFunction.PurePower)" />
</div>
</div>
</div>
@if (ColorProfile.IsHDRProfile)
{
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="minSDR">Minimum SDR brightness (nits)</label>
<input class="form-control" id="minSDR" type="number" min="0" max="10" step="0.001" @bind="ColorProfile.SDRMinBrightness" disabled="@(ColorProfile.SDRTransferFunction != SDRTransferFunction.PurePower)" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="maxSDR">Maximum SDR brightness (nits)</label>
<input class="form-control" id="maxSDR" type="number" min="0" max="10000" @bind="ColorProfile.SDRMaxBrightness" disabled="@(ColorProfile.SDRTransferFunction != SDRTransferFunction.PurePower)" />
</div>
</div>
</div>
}
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="brightnessBoost">Brightness boost (%)</label>
<input class="form-control" id="brightnessBoost" type="number" min="-100" max="100" @bind="ColorProfile.SDRBrightnessBoost" disabled="@(ColorProfile.SDRTransferFunction == SDRTransferFunction.ToneMappedPiecewise)" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="shadowDetailBoost">Shadow detail boost (%)</label>
<input class="form-control" id="shadowDetailBoost" type="number" min="-50" max="200" @bind="ColorProfile.ShadowDetailBoost" disabled="@(ColorProfile.SDRTransferFunction == SDRTransferFunction.ToneMappedPiecewise)" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="shadowDetailRange">Shadow detail range (%)</label>
<input class="form-control" id="shadowDetailRange" type="number" min="0" max="100" @bind="ColorProfile.ShadowDetailRange" disabled="@(ColorProfile.SDRTransferFunction == SDRTransferFunction.ToneMappedPiecewise)" />
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="redGain">Red gain (%)</label>
<input class="form-control" id="redGain" type="number" min="1" max="200" step="0.01" @bind="ColorProfile.RedGain" disabled="@(ColorProfile.SDRTransferFunction == SDRTransferFunction.ToneMappedPiecewise)" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="greenGain">Green gain (%)</label>
<input class="form-control" id="greenGain" type="number" min="1" max="200" step="0.01" @bind="ColorProfile.GreenGain" disabled="@(ColorProfile.SDRTransferFunction == SDRTransferFunction.ToneMappedPiecewise)" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="blueGain">Blue gain (%)</label>
<input class="form-control" id="blueGain" type="number" min="1" max="200" step="0.01" @bind="ColorProfile.BlueGain" disabled="@(ColorProfile.SDRTransferFunction == SDRTransferFunction.ToneMappedPiecewise)" />
</div>
</div>
</div>
@if (ColorProfile.IsHDRProfile)
{
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="hdrluminance">Current HDR Luminance (nits):</label>
<input class="form-control" id="hdrluminance" type="number" min="400" max="10000" @bind="ColorProfile.ToneMappingFromLuminance" disabled="@(ColorProfile.SDRTransferFunction != SDRTransferFunction.ToneMappedPiecewise)" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="hdrluminanceoutput">HDR Luminance to convert (nits):</label>
<input class="form-control" id="hdrluminanceoutput" type="number" min="400" max="10000" @bind="ColorProfile.ToneMappingToLuminance" disabled="@(ColorProfile.SDRTransferFunction != SDRTransferFunction.ToneMappedPiecewise)" />
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="mb-2">
<label class="form-label" for="brightnessmultiplier">Brightness Multiplier</label>
<input class="form-control" id="brightnessmultiplier" type="number" min="0.04" max="25" step=".001" @bind="ColorProfile.HdrBrightnessMultiplier" disabled="@(ColorProfile.SDRTransferFunction != SDRTransferFunction.ToneMappedPiecewise)" />
</div>
</div>
<div class="col">
<div class="mb-2">
<label class="form-label" for="gammamultiplier">Gamma Multiplier</label>
<input class="form-control" id="gammamultiplier" type="number" min="0.04" max="25" step=".001" @bind="ColorProfile.HdrGammaMultiplier" disabled="@(ColorProfile.SDRTransferFunction != SDRTransferFunction.ToneMappedPiecewise)" />
</div>
</div>
</div>
}
</div>
</div>
</div>
</div>
}
</form>
</div>
<div class="modal-footer">
@if (DisplayInfo?.IsAdvancedColorSupported == true && ColorProfile != null)
{
<input class="form-check-input" type="checkbox" @bind="ColorProfile.IsHDRProfile" id="associateAsHdrProfile">
<label class="form-check-label" for="associateAsHdrProfile">
Associate as HDR profile
</label>
}
<button type="submit" class="btn btn-primary" @onclick="InstallOnClick" disabled="@(ColorProfile == null)">Install and set as default</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="CloseOnClick">Close</button>
<button type="submit" class="btn btn-primary" @onclick="ApplyClick" disabled="@(ColorProfile == null)">Save</button>
</div>
</div>
</div>
</div>
@code {
private ColorProfileDto? ColorProfile { get; set; }
private DisplayColorInfo? CurrentColorInfo { get; set; }
private DisplayColorInfo? ColorProfileColorInfo { get; set; }
private List<DisplayColorInfo> DisplayColors { get; set; } = new();
private bool PrimariesDisabled = false;
[Parameter]
public string? ColorProfileName { get; set; }
[Parameter]
public bool UseAsTemplate { get; set; }
[Parameter]
public DisplayInfoDto? DisplayInfo { get; set; }
[Parameter]
public Func<ColorProfileDto, Task>? AfterApply { get; set; }
private string WasValidated = "";
private bool IsVisible;
private DisplayPrimariesSource DisplayPrimariesSource;
protected override async Task OnParametersSetAsync()
{
if (ColorProfileName == null || !IsVisible)
{
return;
}
if (ColorProfileName == "")
{
ColorProfile = ColorProfileDto.CreateDefault(DisplayInfo?.IsHdrSupportedAndEnabled ?? false);
}
else
{
ColorProfile = await _rpcClientService.CallAsync<ColorProfileDto>("ColorProfileService", "GetColorProfile", ColorProfileName, DisplayInfo?.DeviceName);
}
if (ColorProfile != null)
{
ColorProfileColorInfo = new DisplayColorInfo
{
DisplayPrimariesSource = DisplayPrimariesSource.ColorProfile,
BlackLuminance = ColorProfile.BlackLuminance,
WhiteLuminance = ColorProfile.WhiteLuminance,
MinCLL = ColorProfile.MinCLL,
MaxCLL = ColorProfile.MaxCLL,
RgbPrimaries = new RgbPrimariesDto(ColorProfile.DevicePrimaries)
};
CurrentColorInfo = ColorProfileColorInfo;
DisplayColors.Clear();
DisplayColors.Add(CurrentColorInfo);
if (DisplayInfo != null)
{
DisplayColors.AddRange(DisplayInfo.DisplayColors);
}
if (UseAsTemplate)
{
if (ColorProfile.Name != null)
{
ColorProfile.Name = Path.GetFileNameWithoutExtension(ColorProfile.Name) + " (clone).icm";
}
else
{
ColorProfile.Name = "";
}
}
}
}
private async Task ApplyClick(MouseEventArgs e)
{
if (ColorProfile == null || CurrentColorInfo == null)
{
return;
}
if (!(await IsValid()))
{
return;
}
ColorProfile.UpdatePrimariesAndLuminance(CurrentColorInfo);
var result = await _rpcClientService.CallAsync<bool>("ColorProfileService", "UpdateColorProfile", ColorProfile, DisplayInfo?.DeviceName);
if (result)
{
await jsHelper.CloseModal("colorProfileModal");
IsVisible = false;
if (AfterApply != null)
{
await AfterApply.Invoke(ColorProfile);
}
}
}
private async Task InstallOnClick()
{
if (ColorProfile == null || DisplayInfo == null)
{
return;
}
if (!(await IsValid()))
{
return;
}
ColorProfile.UpdatePrimariesAndLuminance(CurrentColorInfo);
var result = await _rpcClientService.CallAsync<bool>("ColorProfileService", "InstallAndActivateColorProfile", ColorProfile, DisplayInfo.DeviceName);
}
private async Task CloseOnClick()
{
IsVisible = false;
if (ColorProfile != null && AfterApply != null)
{
await AfterApply.Invoke(ColorProfile);
}
}
private void ModalOnFocus(FocusEventArgs e)
{
if (!IsVisible)
{
WasValidated = "";
IsVisible = true;
DisplayPrimariesSource = DisplayPrimariesSource.ColorProfile;
}
}
private async Task<bool> IsValid()
{
var valid = await jsHelper.IsFormValid("profileForm");
if (!valid)
{
WasValidated = "was-validated";
}
return valid;
}
private void DisplayPrimariesSourceChanged()
{
if (DisplayInfo == null || ColorProfile == null)
{
return;
}
CurrentColorInfo = DisplayColors.First(dc => dc.DisplayPrimariesSource == DisplayPrimariesSource);
PrimariesDisabled = CurrentColorInfo.DisplayPrimariesSource != DisplayPrimariesSource.ColorProfile;
}
private void OverwritePrimariesOnClick(MouseEventArgs e)
{
if (ColorProfileColorInfo == null || CurrentColorInfo == null)
{
return;
}
ColorProfileColorInfo.BlackLuminance = CurrentColorInfo.BlackLuminance;
ColorProfileColorInfo.WhiteLuminance = CurrentColorInfo.WhiteLuminance;
ColorProfileColorInfo.MinCLL = CurrentColorInfo.MinCLL;
ColorProfileColorInfo.MaxCLL = CurrentColorInfo.MaxCLL;
ColorProfileColorInfo.RgbPrimaries = new RgbPrimariesDto(CurrentColorInfo.RgbPrimaries);
}
private void AfterNameChanged()
{
if (ColorProfile?.Name == null)
{
return;
}
if (!ColorProfile.Name.EndsWith(".icm", StringComparison.InvariantCultureIgnoreCase))
{
ColorProfile.Name += ".icm";
}
}
}
@@ -0,0 +1,355 @@
@page "/ColorProfile"
@using ColorControl.Shared.Common
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.DisplayInfo
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Generics
@using ColorControl.UI.Pages.Generic
@using ColorControl.UI.Services
@inherits SummaryComponentBase
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
@inject JSHelper jsHelper;
<PageTitle>Color profiles</PageTitle>
<ColorProfilePage ColorProfileName="@SelectedColorProfileName" UseAsTemplate="UseAsTemplate" DisplayInfo="SelectedDisplay" AfterApply="AfterColorProfileSaved"></ColorProfilePage>
<ConfirmModal Title="Delete color profile" Message="Are you sure you want to delete the color profile?" OnConfirm="ColorProfileOnDeleteConfirmed" />
<ConfirmModal ModalId="disassociateConfirmModal" Title="Disassociate color profile" Message="Are you sure you want to disassociate the color profile from the selected display?" OnConfirm="ColorProfileOnDisassociateConfirmed" />
<ConfirmModal ModalId="associateConfirmModal" ExtraFields="AssociateFields" Title="Associate color profile" Message="Are you sure you want to associate the color profile with the selected display?" OnConfirm="ColorProfileOnAssociateConfirmed" />
<h2>Displays</h2>
@if (Displays == null)
{
<div>Loading...</div>
}
else
{
<div class="row mb-2">
@if (!Displays.Any())
{
<span>No displays found</span>
}
else
{
<div class="mb-2">
<label class="form-label" for="device">Selected display</label>
<select class="form-select" id="device" @bind="SelectedDisplayId" @bind:after="SelectedDisplayChanged">
@foreach (var display in Displays)
{
<option value="@display.DisplayId">@display.DisplayName @@ @display.AdapterName</option>
}
</select>
</div>
}
</div>
}
@if (SelectedDisplay == null)
{
<div>Loading...</div>
}
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">
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => AddColorProfileOnClick()">
Add
</button>
</div>
</div>
<div class="row mb-3">
<div class="col">
@if (!SelectedDisplay.ColorProfiles.Any())
{
<span>No color profiles found that are associated with the display</span>
}
else
{
<div class="list-group">
@foreach (var colorProfile in SelectedDisplay.ColorProfiles)
{
<span class="list-group-item list-group-item-action" @onclick="() => SetColorProfileOnClick(colorProfile)" @onclick:preventDefault @onclick:stopPropagation>
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="mb-1">
@if (colorProfile.IsHDRProfile)
{
<span class="me-2 badge text-bg-info" title="HDR profile">HDR</span>
}
else
{
<span class="me-2 badge text-bg-secondary" title="SDR profile">SDR</span>
}
@colorProfile.Name
@if (colorProfile.IsHDRProfile && colorProfile.Name == SelectedDisplay?.DefaultHdrColorProfile)
{
<span class="ms-2 badge text-bg-warning" title="Active HDR color profile">Active HDR</span>
}
else if (!colorProfile.IsHDRProfile && colorProfile.Name == SelectedDisplay?.DefaultSdrColorProfile)
{
<span class="ms-2 badge text-bg-warning" title="Active SDR color profile">Active SDR</span>
}
</h5>
</div>
<span class="d-none d-lg-block">
<button class="btn btn-warning btn-sm" data-bs-toggle="modal" data-bs-target="#disassociateConfirmModal" @onclick="() => SelectedColorProfile = colorProfile" @onclick:preventDefault @onclick:stopPropagation>Disassociate</button>
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => DeleteColorProfileOnClick(colorProfile.Name)" @onclick:preventDefault @onclick:stopPropagation>Delete</button>
<button class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => CloneColorProfileOnClick(colorProfile.Name)" @onclick:preventDefault @onclick:stopPropagation>Clone</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => EditColorProfileOnClick(colorProfile.Name)" @onclick:preventDefault @onclick:stopPropagation>Edit</button>
</span>
<div class="d-lg-none" @onclick:preventDefault @onclick:stopPropagation>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#disassociateConfirmModal" @onclick="() => SelectedColorProfileName = colorProfile.Name">Disassociate</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => DeleteColorProfileOnClick(colorProfile.Name)">Delete</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => CloneColorProfileOnClick(colorProfile.Name)">Clone</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => EditColorProfileOnClick(colorProfile.Name)">Edit</a></li>
</ul>
</div>
</div>
</span>
}
</div>
}
</div>
</div>
}
<div class="mb-2 d-flex align-items-center">
<h2>Other Color profiles</h2>
</div>
<div class="row mb-3">
<div class="col">
@if (ColorProfileNames == null)
{
<span>No profiles found</span>
}
else
{
<div class="list-group">
@foreach (var colorProfile in ColorProfileNames)
{
<span class="list-group-item list-group-item-action" @onclick="() => EditColorProfileOnClick(colorProfile)" @onclick:preventDefault @onclick:stopPropagation>
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="mb-1">
@colorProfile
</h5>
</div>
<span class="d-none d-lg-block">
<button class="btn btn-warning btn-sm" data-bs-toggle="modal" data-bs-target="#associateConfirmModal" @onclick="() => AssociateColorProfileOnClick(colorProfile)" @onclick:preventDefault @onclick:stopPropagation>Associate</button>
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => DeleteColorProfileOnClick(colorProfile)" @onclick:preventDefault @onclick:stopPropagation>Delete</button>
<button class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => CloneColorProfileOnClick(colorProfile)" @onclick:preventDefault @onclick:stopPropagation>Clone</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => EditColorProfileOnClick(colorProfile)" @onclick:preventDefault @onclick:stopPropagation>Edit</button>
</span>
<div class="d-lg-none" @onclick:preventDefault @onclick:stopPropagation>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#associateConfirmModal" @onclick="() => AssociateColorProfileOnClick(colorProfile)">Associate</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => DeleteColorProfileOnClick(colorProfile)">Delete</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => CloneColorProfileOnClick(colorProfile)">Clone</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#colorProfileModal" @onclick="() => EditColorProfileOnClick(colorProfile)">Edit</a></li>
</ul>
</div>
</div>
</span>
}
</div>
}
</div>
</div>
@code {
private string? SelectedDisplayId;
private DisplayInfoDto? SelectedDisplay;
private List<DisplayInfoDto>? Displays;
private string? SelectedColorProfileName;
private ColorProfileDto? SelectedColorProfile;
private bool UseAsTemplate;
private List<string>? ColorProfileNames;
private List<FieldDefinition>? AssociateFields;
private FieldDefinition AskHdrAssociationField = new FieldDefinition { Label = "Associate as HDR profile", FieldType = FieldType.CheckBox, Value = false };
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await Refresh();
}
protected override async Task Refresh()
{
await RefreshDisplays();
}
private void AddColorProfileOnClick()
{
SelectedColorProfileName = "";
}
private async Task EditColorProfileOnClick(string colorProfile)
{
UseAsTemplate = false;
SelectedColorProfileName = colorProfile;
await jsHelper.OpenModal("colorProfileModal");
}
private async Task SetColorProfileOnClick(ColorProfileDto colorProfile)
{
if (SelectedDisplay == null)
{
return;
}
var result = await _rpcClientService.CallAsync<bool>("ColorProfileService", "SetDisplayDefaultColorProfile", SelectedDisplay.DeviceName, colorProfile.Name, Type.Missing, colorProfile.IsHDRProfile);
if (result)
{
if (colorProfile.IsHDRProfile)
{
SelectedDisplay.DefaultHdrColorProfile = colorProfile.Name;
}
else
{
SelectedDisplay.DefaultSdrColorProfile = colorProfile.Name;
}
}
}
private void AssociateColorProfileOnClick(string colorProfile)
{
SelectedColorProfileName = colorProfile;
if (SelectedDisplay?.IsAdvancedColorSupported == true)
{
AskHdrAssociationField.Value = false;
AssociateFields = [AskHdrAssociationField];
}
else
{
AssociateFields = null;
}
}
private void DeleteColorProfileOnClick(string colorProfile)
{
SelectedColorProfileName = colorProfile;
}
private void CloneColorProfileOnClick(string colorProfile)
{
UseAsTemplate = true;
SelectedColorProfileName = colorProfile;
}
private async Task ColorProfileOnDeleteConfirmed()
{
if (SelectedColorProfileName == null)
{
return;
}
await _rpcClientService.CallAsync<bool>("ColorProfileService", "DeleteColorProfile", SelectedColorProfileName);
SelectedColorProfileName = null;
await RefreshDisplays();
StateHasChanged();
}
private async Task ColorProfileOnDisassociateConfirmed()
{
if (SelectedColorProfile == null || SelectedDisplay == null)
{
return;
}
await _rpcClientService.CallAsync<bool>("ColorProfileService", "RemoveDisplayColorProfile", SelectedDisplay.DeviceName, SelectedColorProfile.Name, SelectedColorProfile.IsHDRProfile);
SelectedColorProfile = null;
await RefreshDisplays();
StateHasChanged();
}
private async Task ColorProfileOnAssociateConfirmed()
{
if (SelectedColorProfileName == null || SelectedDisplay == null)
{
return;
}
await _rpcClientService.CallAsync<bool>("ColorProfileService", "AddDisplayColorProfile", SelectedDisplay.DeviceName, SelectedColorProfileName, false, AskHdrAssociationField.BoolValue);
SelectedColorProfileName = null;
await RefreshDisplays();
StateHasChanged();
}
private async Task SelectedDisplayChanged()
{
SelectedDisplay = Displays?.FirstOrDefault(d => d.DisplayId == SelectedDisplayId);
ColorProfileNames = await _rpcClientService.CallAsync<List<string>>("ColorProfileService", "GetAllColorProfileNames");
if (SelectedDisplay != null)
{
ColorProfileNames = ColorProfileNames?.Except(SelectedDisplay.ColorProfiles.Select(p => p.Name)).ToList();
}
}
private async Task AfterColorProfileSaved(ColorProfileDto colorProfile)
{
await RefreshDisplays();
StateHasChanged();
}
private async Task RefreshDisplays()
{
Displays = await _rpcClientService.CallAsync<List<DisplayInfoDto>>("ColorProfileService", "GetDisplayInfos");
if (Displays?.Any() == true)
{
SelectedDisplayId = (Displays.FirstOrDefault(d => d.DisplayId == SelectedDisplayId) ?? Displays.First()).DisplayId;
}
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();
}
}
}
@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}
@@ -0,0 +1,242 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.Game
@using ColorControl.UI.Services
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.Shared.Common
@rendermode Constants.RenderMode
@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-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>
<StartProgramInput @bind-Path="Preset.Path" @bind-Parameters="Preset.Parameters" @bind-Path:after="PathChanged"></StartProgramInput>
<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();
private List<GameApp>? RecentApps;
private string RecentAppFilename = "";
public bool SelectingFile { get; set; }
private string WasValidated = "";
private bool IsVisible;
protected override async Task OnParametersSetAsync()
{
if (PresetParam == null)
{
return;
}
Preset = PresetParam.CloneWithId();
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;
}
}
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 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;
}
}
private void PathChanged()
{
if (Preset == null)
{
return;
}
if (Preset.Path != null && string.IsNullOrEmpty(Preset.name))
{
Preset.name = Path.GetFileNameWithoutExtension(Preset.Path);
}
}
}
@@ -0,0 +1,61 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.Game
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
<div class="modal" id="settingsModal" tabindex="-1">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">NVIDIA settings</h5>
<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,178 @@
@page "/Game"
@using ColorControl.Shared.Contracts;
@using ColorControl.Shared.Contracts.Game;
@using ColorControl.UI.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.Shared.Common;
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@rendermode Constants.RenderMode
@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">
@if (!Presets.Any())
{
<span>No presets found</span>
}
<div class="list-group">
@foreach (var preset in Presets)
{
<span 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>
}
@if (!preset.PathExists)
{
<span class="ms-2 badge text-bg-danger" title="@($"Path '{preset.Path}' is not found")">Not found</span>
}
</h5>
</div>
<span class="d-none d-lg-block">
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Delete</button>
<button class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Clone</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Edit</button>
</span>
<div class="d-lg-none" @onclick:preventDefault @onclick:stopPropagation>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)">Delete</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)">Clone</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)">Edit</a></li>
</ul>
</div>
</div>
</span>
}
</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 void PresetOnEditClick(GamePreset preset)
{
CurrentPreset = preset;
}
private void PresetOnCloneClick(GamePreset preset)
{
CurrentPreset = new GamePreset(preset);
CurrentPreset.SetCloneValues();
}
private void 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 void 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,50 @@
@using ColorControl.Shared.Contracts
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@rendermode Constants.RenderMode
<FieldsModal ModalId="@ModalId" Fields="Fields" Title="@Title" OnConfirm="ConfirmClick" NextModalId="@NextModalId"></FieldsModal>
@code {
[Parameter]
public string? Title { get; set; }
[Parameter]
public string? Message { get; set; }
[Parameter]
public Func<Task>? OnConfirm { get; set; }
[Parameter]
public string ModalId { get; set; } = "confirmModal";
[Parameter]
public string? NextModalId { get; set; }
[Parameter]
public List<FieldDefinition>? ExtraFields { get; set; }
public List<FieldDefinition>? Fields { get; set; }
protected override void OnParametersSet()
{
if (Message != null)
{
Fields = [new FieldDefinition { FieldType = FieldType.Label, Label = Message }];
if (ExtraFields != null)
{
Fields.AddRange(ExtraFields);
}
}
}
private async Task ConfirmClick()
{
if (OnConfirm != null)
{
await OnConfirm();
}
}
}
@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Components;
namespace ColorControl.UI.Pages.Generic;
public abstract class CustomComponentBase : ComponentBase, IDisposable
{
public void Dispose()
{
GC.SuppressFinalize(this);
Blazor.ClearCurrentComponent(this);
}
protected override Task OnInitializedAsync()
{
Blazor.SetCurrentComponent(this);
return Task.CompletedTask;
}
}
@@ -0,0 +1,122 @@
@using ColorControl.Shared.Contracts
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@rendermode Constants.RenderMode
@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" @bind:after="() => FieldBindAfter(field)">
@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()}";
<RangeInput TValue="int" @bind-Value="field.IntValue" Id="@fieldId" Label="@field.Label" Min="@((int)field.MinValue)" Max="@((int)field.MaxValue)" SubLabel="@field.SubLabel" />
}
}
@code {
[Parameter]
public List<FieldDefinition>? Fields { get; set; }
[Parameter]
public EventCallback<FieldDefinition> FieldValueChanged { get; set; }
private async Task FieldBindAfter(FieldDefinition field)
{
await FieldValueChanged.InvokeAsync(field);
}
}
@@ -0,0 +1,134 @@
@using ColorControl.Shared.Contracts
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@rendermode Constants.RenderMode
@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">
<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? NextModalId { 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;
if (!string.IsNullOrEmpty(NextModalId))
{
await jsHelper.OpenModal(NextModalId);
}
}
}
@@ -0,0 +1,53 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.EventDispatcher
@using ColorControl.UI.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,96 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.EventDispatcher
@using ColorControl.Shared.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Services
@rendermode Constants.RenderMode
@inject JSHelper jsHelper;
@inject NotificationService notificationService;
@inject NavigationManager _navigationManager;
@if (Notifications?.Any() == true)
{
<div aria-live="polite" aria-atomic="true" class="position-relative">
<div class="toast-container position-fixed bottom-0 end-0 p-3">
@foreach (var notification in Notifications)
{
<div class="toast" id="toast_@notification.Id" role="alert" aria-live="assertive" aria-atomic="true" @onclick="() => NotificationOnClick(notification)">
<div class="toast-header" style="background-color: var(--bs-@notification.Level-bg-subtle)">
<span class="w-100 d-flex">
<div class="alert alert-@notification.Level mb-0 py-1">
<strong class="me-auto">@notification.Category</strong>
</div>
</span>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
@if (notification.InternalUrl != null)
{
<ul class="list-group-flush p-0 mb-0">
<li class="list-group-item">@notification.Message</li>
</ul>
}
else
{
@notification.Message
}
</div>
</div>
}
</div>
</div>
}
@code {
private List<NotificationDto> Notifications { get; } = [];
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
notificationService.OnNotification += AddNotification;
}
}
protected async Task AddNotification(NotificationDto notification)
{
RemoveOldNotifications();
var minShownAt = DateTime.UtcNow.AddSeconds(-5);
// Suppress duplicate noficiations
if (Notifications.Any(n => n.Message == notification.Message && n.Level == notification.Level && n.ShownAt >= minShownAt))
{
return;
}
Notifications.Add(notification);
await InvokeAsync(StateHasChanged);
await jsHelper.ShowToast($"toast_{notification.Id}");
notification.ShownAt = DateTime.UtcNow;
}
private void NotificationOnClick(NotificationDto notification)
{
if (notification.InternalUrl != null)
{
_navigationManager.NavigateTo(notification.InternalUrl);
}
Notifications.Remove(notification);
}
private void RemoveOldNotifications()
{
var minDateTime = DateTime.UtcNow.AddMinutes(-1);
Notifications.RemoveAll(n => n.ShownAt.HasValue && n.ShownAt <= minDateTime);
}
}
@@ -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,115 @@
@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);
}
}
@@ -0,0 +1,140 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.EventDispatcher
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@rendermode Constants.RenderMode
@if (Label != null)
{
<div class="mb-2">
<label class="form-label" for="name">@Label</label>
<div class="input-group">
<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"/>
<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)
{
<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
</a>
</li>
}
</ul>
</div>
</div>
}
@code {
[Parameter]
public string? Label { get; set; }
[Parameter]
public string? Shortcut { get; set; }
[Parameter]
public EventCallback<string?> ShortcutChanged { get; set; }
private Dictionary<string, bool> Modifiers = new() {
{ "Shift", false },
{ "Control", false },
{ "Alt", false },
{ "Win", false }
};
protected override void OnParametersSet()
{
LoadModifiers();
}
private async Task ShortcutOnChange(ChangeEventArgs e)
{
Shortcut = e.Value?.ToString();
LoadModifiers();
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;
}
private void LoadModifiers()
{
foreach (var modifier in Modifiers.Keys)
{
Modifiers[modifier] = Shortcut?.Contains(modifier) == true;
}
}
private async Task ModifierKeyChanged(ChangeEventArgs e, string modifier)
{
var value = e.Value is bool boolValue && boolValue;
Modifiers[modifier] = value;
await SetModifiers();
}
private async Task SetModifiers()
{
var shortcut = Shortcut ?? "";
foreach (var modifier in Modifiers.Keys)
{
shortcut = shortcut.Replace(modifier, "").Trim().TrimStart(',');
}
shortcut = shortcut.Trim();
foreach (var modifier in Modifiers.Where(kv => kv.Value).Select(kv => kv.Key).Reverse())
{
if (shortcut.StartsWith("+"))
{
shortcut = $"{modifier} {shortcut}";
}
else
{
shortcut = $"{modifier}, {shortcut}";
}
}
Shortcut = shortcut;
await ShortcutChanged.InvokeAsync(Shortcut);
}
private async Task ModifierOnClick(string modifier)
{
Modifiers[modifier] = !Modifiers[modifier];
await SetModifiers();
}
}
@@ -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;
}
}
}
@@ -0,0 +1,651 @@
@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.UI.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 || LgPresets != null || CustomItems != null)
{
<div class="dropdown dropstart 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-3 dropdown-menu-end" style="width: 21rem; height: 430px">
<ul class="nav nav-tabs" id="presetTab" role="tablist">
@{
var linkActive = "active";
}
@if (NvPresets != null && NvPresetsEnabled)
{
<li class="nav-item" role="presentation">
<button class="nav-link px-2 @(linkActive)" id="nvidia-tab" data-bs-toggle="tab" data-bs-target="#nvidia-tab-pane" type="button" role="tab" aria-controls="nvidia-tab-pane" aria-selected="true">NVIDIA</button>
</li>
linkActive = "";
}
@if (SamsungPresets != null && SamsungPresetsEnabled)
{
<li class="nav-item" role="presentation">
<button class="nav-link px-2 @(linkActive)" id="samsung-tab" data-bs-toggle="tab" data-bs-target="#samsung-tab-pane" type="button" role="tab" aria-controls="samsung-tab-pane" aria-selected="true">Samsung</button>
</li>
linkActive = "";
}
@if (LgPresets != null && LgPresetsEnabled)
{
<li class="nav-item" role="presentation">
<button class="nav-link px-2 @(linkActive)" id="lg-tab" data-bs-toggle="tab" data-bs-target="#lg-tab-pane" type="button" role="tab" aria-controls="lg-tab-pane" aria-selected="true">LG</button>
</li>
linkActive = "";
}
@if (CustomItems != null)
{
<li class="nav-item" role="presentation">
<button class="nav-link px-2 @(linkActive)" id="custom-tab" data-bs-toggle="tab" data-bs-target="#custom-tab-pane" type="button" role="tab" aria-controls="custom-tab-pane" aria-selected="true">Custom</button>
</li>
linkActive = "";
}
<li class="nav-item" role="presentation">
<button class="nav-link px-2 @(linkActive)" id="program-tab" data-bs-toggle="tab" data-bs-target="#program-tab-pane" type="button" role="tab" aria-controls="program-tab-pane" aria-selected="true">Program</button>
</li>
@if (ActionItems != null)
{
linkActive = "";
<li class="nav-item" role="presentation">
<button class="nav-link px-2 @(linkActive)" id="action-tab" data-bs-toggle="tab" data-bs-target="#action-tab-pane" type="button" role="tab" aria-controls="action-tab-pane" aria-selected="true">Action</button>
</li>
}
</ul>
<div class="tab-content" id="presetTabContent">
@{
var tabActive = "show active";
}
@if (NvPresets != null && NvPresetsEnabled)
{
<div class="tab-pane fade @(tabActive)" id="nvidia-tab-pane" role="tabpanel" aria-labelledby="nvidia-tab">
<div class="mt-3 mb-3">
<label class="form-label" for="nvPreset">NVIDIA preset</label>
<select class="form-select" multiple id="nvPreset" @onchange="NvPresetChange" style="height: 250px">
@foreach (var preset in NvPresets)
{
<option value="@preset.id">@preset.IdOrName</option>
}
</select>
</div>
<button type="button" class="btn btn-sm btn-primary" @onclick="() => AddNvPresetOnClick()" disabled="@(NvPresetsToAdd.Count == 0)">Add item(s)</button>
</div>
tabActive = "";
}
@if (SamsungPresets != null && SamsungPresetsEnabled)
{
<div class="tab-pane fade @(tabActive)" id="samsung-tab-pane" role="tabpanel" aria-labelledby="samsung-tab">
<div class="mt-3 mb-3">
<label class="form-label" for="samsungPreset">Samsung preset</label>
<select class="form-select" id="samsungPreset" @bind="SamsungPresetToAddId">
<option value="">Select preset</option>
@foreach (var preset in SamsungPresets)
{
<option value="@preset.id">@preset.IdOrName</option>
}
</select>
</div>
<button type="button" class="btn btn-sm btn-primary" @onclick="() => AddSamsungPresetOnClick()">Add item</button>
</div>
tabActive = "";
}
@if (LgPresets != null && LgPresetsEnabled)
{
<div class="tab-pane fade @(tabActive)" id="lg-tab-pane" role="tabpanel" aria-labelledby="lg-tab">
<div class="mt-3 mb-3">
<label class="form-label" for="lgPreset">LG preset</label>
<select class="form-select" id="lgPreset" @bind="LgPresetToAddId">
<option value="">Select preset</option>
@foreach (var preset in LgPresets)
{
<option value="@preset.id">@preset.IdOrName</option>
}
</select>
</div>
<button type="button" class="btn btn-sm btn-primary" @onclick="() => AddLgPresetOnClick()">Add item</button>
</div>
tabActive = "";
}
@if (CustomItems != null)
{
<div class="tab-pane fade @(tabActive)" id="custom-tab-pane" role="tabpanel" aria-labelledby="custom-tab">
@foreach (var customItem in CustomItems)
{
<div class="mt-3 mb-3">
<label class="form-label" for="customItem">@customItem.Name</label>
<select class="form-select" id="customItem" @bind="customItem.SelectedItem">
<option value="">Select item</option>
@foreach (var item in customItem.Items)
{
<option value="@item">@item</option>
}
</select>
</div>
}
<button type="button" class="btn btn-sm btn-primary" @onclick="() => AddCustomItemsOnClick()">Add item</button>
</div>
tabActive = "";
}
<div class="tab-pane fade @(tabActive)" id="program-tab-pane" role="tabpanel" aria-labelledby="program-tab">
<div class="mt-3 mb-3">
<StartProgramInput @bind-Path="StartProgramPath" @bind-Parameters="StartProgramParameters"></StartProgramInput>
</div>
<button type="button" class="btn btn-sm btn-primary" @onclick="() => AddStartProgramOnClick()">Add item</button>
</div>
@{
tabActive = "";
}
@if (ActionItems != null)
{
<div class="tab-pane fade @(tabActive)" id="action-tab-pane" role="tabpanel" aria-labelledby="action-tab">
<div class="mt-3 mb-3">
@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="">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>
}
</div>
<button type="button" class="btn btn-sm btn-primary" @onclick="() => AddActionItemsOnClick()">Add item</button>
</div>
}
</div>
</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" @onclick="() => PresetStepClick(step)">@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" title="Move left" disabled="@(stepIndex == 0)" @onclick="() => MoveLeftOnClick(stepIndex)">&lt;</button>
<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>
<RangeInput TValue="int" @bind-Value="step.Delay" Id="delay" Label="Delay" Unit="ms" Max="10000" @bind-Value:after="() => AfterDelayChanged()" />
@if (ActionFieldsEdit != null)
{
<FieldDefs Fields="ActionFieldsEdit" FieldValueChanged="ActionFieldValueChanged"></FieldDefs>
}
</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;
private PresetStep? CurrentPresetStep;
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? SamsungPresetToAddId { get; set; }
private int? LgPresetToAddId { get; set; }
private List<PresetStep> PresetSteps = new();
private string? RawText;
private List<FieldDefinition>? ActionFields;
private List<FieldDefinition>? ActionFieldsEdit;
private string? StartProgramPath;
private string? StartProgramParameters;
private List<int> NvPresetsToAdd = [];
protected override void OnParametersSet()
{
if (Steps != null)
{
var stepIndex = CurrentPresetStep != null ? PresetSteps.IndexOf(CurrentPresetStep) : -1;
PresetSteps.Clear();
PresetSteps.AddRange(Steps.Select(s => PresetStep.Parse(s)));
RawText = string.Join(", ", PresetSteps);
if (stepIndex > -1 && stepIndex < PresetSteps.Count)
{
CurrentPresetStep = PresetSteps[stepIndex];
}
}
}
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 MoveLeftOnClick(int index)
{
if (PresetSteps.Count > index && index > 0)
{
var step = PresetSteps[index];
PresetSteps.RemoveAt(index);
PresetSteps.Insert(index - 1, step);
}
await EmitSteps();
}
private async Task MoveRightOnClick(int index)
{
if (PresetSteps.Count > index + 1)
{
var step = PresetSteps[index];
PresetSteps.RemoveAt(index);
PresetSteps.Insert(index + 1, step);
}
await EmitSteps();
}
private async Task AfterDelayChanged()
{
await EmitSteps();
}
private async Task AddNvPresetOnClick()
{
NvPresetsToAdd.ForEach(id => AddPreset(NvPresets, "NvPreset", id));
await EmitSteps();
}
private async Task AddSamsungPresetOnClick()
{
AddPreset(SamsungPresets, "SamsungPreset", SamsungPresetToAddId);
await EmitSteps();
}
private async Task AddLgPresetOnClick()
{
AddPreset(LgPresets, "LgPreset", LgPresetToAddId);
await EmitSteps();
}
private async Task AddCustomItemsOnClick()
{
if (CustomItems != null && Steps != null)
{
foreach (var customItem in CustomItems)
{
if (string.IsNullOrWhiteSpace(customItem.SelectedItem))
{
continue;
}
PresetSteps.Add(PresetStep.Parse(customItem.SelectedItem));
}
}
await EmitSteps();
}
private async Task AddActionItemsOnClick()
{
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));
}
}
await EmitSteps();
}
private async Task AddStartProgramOnClick()
{
AddStartProgram();
await EmitSteps();
}
private async Task AddPresetOnClick()
{
NvPresetsToAdd.ForEach(id => AddPreset(NvPresets, "NvPreset", id));
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;
}
}
AddStartProgram();
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 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)
{
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 void 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;
}
private void NvPresetChange(ChangeEventArgs eventArgs)
{
if (eventArgs.Value is IEnumerable<string> items)
{
NvPresetsToAdd.Clear();
NvPresetsToAdd.AddRange(items.Select(i => int.Parse(i)));
}
}
private void PresetStepClick(PresetStep step)
{
CurrentPresetStep = step;
var action = ActionItems?.SelectMany(ai => ai.Actions).FirstOrDefault(a => a.Name.Equals(step.ActionName, StringComparison.OrdinalIgnoreCase));
if (action == null)
{
ActionFieldsEdit = null;
return;
}
var field = CreateField(action);
field.StringValue = step.ActionValue;
ActionFieldsEdit = [field];
}
private async Task ActionFieldValueChanged(FieldDefinition field)
{
if (CurrentPresetStep == null)
{
return;
}
CurrentPresetStep.ActionValue = field.StringValue;
await EmitSteps();
}
}
@@ -0,0 +1,13 @@
namespace ColorControl.UI.Pages.Generic;
public abstract class SummaryComponentBase : CustomComponentBase
{
protected abstract Task Refresh();
public async Task RefreshExternal()
{
await Refresh();
await InvokeAsync(StateHasChanged);
}
}
@@ -0,0 +1,65 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.EventDispatcher
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@rendermode Constants.RenderMode
@if (Triggers != null)
{
if (Triggers.Count < 2)
{
<div>
<label class="form-label">Add trigger:</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="clearfix">
<label class="form-label">Trigger @(Triggers.IndexOf(trigger) + 1)</label>
<button class="btn btn-danger btn-sm float-end" type="button" @onclick="() => Triggers.Remove(trigger)">Remove</button>
</div>
<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="value => ConditionsChanged(value, trigger)"></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>
<div class="mb-2">
<label class="form-label" for="connectedDisplays">Connected displays Regex</label>
<input class="form-control" type="text" @bind="trigger.ConnectedDisplaysRegex"
title="The preset will be executed only if the supplied Regular Expression matches the device name of at least one of the displays that are connected at the moment the trigger is fired." />
</div>
@if (Triggers.IndexOf(trigger) < Triggers.Count - 1)
{
<div class="border-bottom mb-2"></div>
}
}
}
@code {
[Parameter]
public List<PresetTrigger>? Triggers { get; set; }
private List<string> ConditionLabels = Utils.GetDescriptions<PresetConditionType>(fromValue: 1).ToList();
public void ConditionsChanged(uint value, PresetTrigger trigger)
{
trigger.Conditions = (PresetConditionType)value;
}
}
@@ -0,0 +1,89 @@
@page "/"
@using ColorControl.Shared.Common
@using ColorControl.Shared.Contracts
@using ColorControl.UI.Services;
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
@inject NotificationService _notificationService
@inject ProtectedSessionStorage _sessionStorage
<PageTitle>Home</PageTitle>
<h1>Color Control</h1>
<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-lg-4 col-xl mb-3">
<div class="card h-100">
<div class="card-header">
<span class="me-2 bi bi-@module.GetIconClass()" aria-hidden="true"></span>@module.DisplayName
</div>
<div class="card-body">
<a href="@module.Link" class="btn btn-primary stretched-link mb-2">Open module</a>
<ul>
@foreach (var info in module.Info)
{
<li>@info</li>
}
</ul>
</div>
</div>
</div>
}
</div>
}
@code {
private Config? Config;
private List<ModuleDto>? Modules;
private InfoDto? Info;
private bool IsUpdateAvailable;
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]);
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender || Info != null || Config?.CheckForUpdates != true)
{
return;
}
var checkedForUpdates = await _sessionStorage.TryGet<bool>("checkedForUpdates");
if (!checkedForUpdates)
{
Info = await _rpcClientService.CallAsync<InfoDto>("OptionsService", "GetInfo");
await _sessionStorage.SetAsync("checkedForUpdates", true);
if (!IsUpdateAvailable && Info?.UpdateInfoDto?.UpdateAvailable == true)
{
IsUpdateAvailable = true;
var message = $"Update available: version {Info.UpdateInfoDto.NewVersionNumber}";
_notificationService.SendNotification(new NotificationDto(message) { InternalUrl = "/info", ScheduledAt = DateTime.UtcNow.AddSeconds(5) });
}
}
}
}
+120
View File
@@ -0,0 +1,120 @@
@page "/info"
@using ColorControl.Shared.Common;
@using ColorControl.UI.Services;
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
@inject NavigationManager _navigationManager;
@inject IHttpContextAccessor _httpContextAccessor
<PageTitle>Logging</PageTitle>
<h1>Information</h1>
@if (info == null)
{
<div>Loading...</div>
}
else
{
<h3 class="mb-2">@info.ApplicationTitle - @info.LegalCopyright</h3>
<div class="d-flex align-items-baseline">
@if (IsUpdateInstalled)
{
if (IsEmbeddedBrowser)
{
<button type="button" class="btn btn-primary me-2" @onclick="RestartOnClick">Restart</button>
}
else
{
<button type="button" class="btn btn-primary me-2" @onclick="ReloadOnClick">Reload</button>
}
<span>New version @info.UpdateInfoDto.NewVersionNumber is installed. Click the button to restart the application.</span>
}
else
{
<button type="button" class="btn btn-primary me-2" disabled="@(!info.UpdateInfoDto.UpdateAvailable || IsInstallingUpdate)" @onclick="InstallUpdateOnClick">
@if (IsInstallingUpdate)
{
<span class="spinner-border spinner-border-sm me-1" aria-hidden="true"></span>
}
<span role="status">Install update</span>
</button>
@if (info.UpdateInfoDto.UpdateAvailable)
{
<span>New version @info.UpdateInfoDto.NewVersionNumber available. Click the button to install the update.</span>
}
else
{
<span>You have the latest version</span>
}
}
</div>
<br/>
<h5>This app contains the following 3rd party plugins:</h5>
<ul class="list-group">
<li class="list-group-item">lgtv.net by gr4b4z</li>
<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;
private bool IsInstallingUpdate;
private bool IsUpdateInstalled;
private bool IsEmbeddedBrowser;
protected override async Task OnInitializedAsync()
{
info = await _rpcClientService.CallAsync<InfoDto>("OptionsService", "GetInfo");
}
private async Task InstallUpdateOnClick(MouseEventArgs e)
{
IsInstallingUpdate = true;
try
{
var result = await _rpcClientService.CallAsync<bool>("OptionsService", "InstallUpdate");
if (!result)
{
return;
}
IsUpdateInstalled = true;
IsEmbeddedBrowser = _httpContextAccessor.HttpContext?.Request.Headers["user-agent"].ToString().Contains("ColorControlEmbedded") == true;
}
finally
{
IsInstallingUpdate = false;
}
}
private async Task RestartOnClick(MouseEventArgs e)
{
var result = await _rpcClientService.CallAsync<bool>("OptionsService", "RestartAfterUpdate");
}
private async Task ReloadOnClick(MouseEventArgs e)
{
var result = await _rpcClientService.CallAsync<bool>("OptionsService", "RestartAfterUpdate");
_navigationManager.Refresh();
}
}
@@ -0,0 +1,247 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.LG
@using ColorControl.UI.Services
@using Microsoft.AspNetCore.Components.Web
@using ColorControl.Shared.Common
@using ColorControl.UI.Components.Pages.Generic
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
<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">
<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 py-2" 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="checkPowerOnAfterSessionUnlock" @bind="Device.Options.PowerOnAfterSessionUnlock">
<label class="form-check-label" for="checkPowerOnAfterSessionUnlock">Power on when unlocking the pc</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOffAfterSessionLock" @bind="Device.Options.PowerOffAfterSessionLock">
<label class="form-check-label" for="checkPowerOffAfterSessionLock">Power off when locking the pc</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 class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkUseNewHandshake" @bind="Device.Options.UseNewHandshake">
<label class="form-check-label" for="checkUseNewHandshake">Enable webOS 26 compatibility</label>
</div>
</div>
</div>
</div>
</div>
}
</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>
</div>
</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;
[Parameter]
public LgDeviceDto? Device { get; set; }
[Parameter]
public Func<LgDeviceDto, Task>? AfterApply { get; set; }
private bool IsTestingConnection;
private bool? IsTestConnectionSuccessful;
protected override void OnParametersSet()
{
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 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;
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.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode Constants.RenderMode
@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 void OnParametersSet()
{
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,181 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.LG
@using ColorControl.UI.Services
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.Shared.Common
@using static ColorControl.UI.Components.Pages.Generic.StepsInput<ColorControl.Shared.Contracts.LG.LgPreset>
@rendermode Constants.RenderMode
@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-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
{
<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 void OnParametersSet()
{
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.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode Constants.RenderMode
@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,398 @@
@page "/LG"
@using ColorControl.Shared.Common
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.LG
@using ColorControl.UI.Components.Pages.RemoteControl
@using ColorControl.UI.Pages.Generic
@using ColorControl.UI.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inherits SummaryComponentBase
@rendermode Constants.RenderMode
@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">
<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
</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>
<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" />
@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="#lgDeviceModal" @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="#lgDeviceModal" @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">
@if (!Presets.Any())
{
<span>No presets found</span>
}
<div class="list-group">
@foreach (var preset in Presets)
{
<span class="list-group-item list-group-item-action" @onclick="() => PresetOnClick(preset)" @onclick:preventDefault @onclick:stopPropagation>
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="mb-1">@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 class="d-none d-lg-block">
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Delete</button>
<button class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Clone</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Edit</button>
</span>
<div class="d-lg-none" @onclick:preventDefault @onclick:stopPropagation>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)">Delete</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)">Clone</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)">Edit</a></li>
</ul>
</div>
</div>
</span>
}
</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;
private List<LgPreset> RcPresets = [LgPresets.InStartPreset, LgPresets.EzAdjustPreset];
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await Refresh();
}
protected override async Task Refresh()
{
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() ?? new List<ActionGroup>();
}
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 void PresetOnEditClick(LgPreset preset)
{
CurrentPreset = preset;
}
private void PresetOnCloneClick(LgPreset preset)
{
CurrentPreset = new LgPreset(preset);
CurrentPreset.SetCloneValues();
}
private void 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 void DeviceOnClick(LgDeviceDto device)
{
CurrentDevice = device;
}
private void AddDeviceOnClick()
{
CurrentDevice = new LgDeviceDto { IsCustom = true };
}
private void AddPresetOnClick()
{
CurrentPreset = new LgPreset();
}
private async Task AfterPresetApplied(LgPreset preset)
{
await RefreshPresets();
StateHasChanged();
}
private async Task AfterDeviceApplied(LgDeviceDto device)
{
Devices = await _rpcClientService.CallAsync<List<LgDeviceDto>>("LgService", "GetDevices");
StateHasChanged();
}
private async Task RefreshDevicesOnClick()
{
IsRefreshDisabled = true;
try
{
try
{
await _rpcClientService.CallAsync<bool>("LgService", "RefreshDevices");
}
catch
{
}
Devices = await _rpcClientService.CallAsync<List<LgDeviceDto>>("LgService", "GetDevices");
}
finally
{
IsRefreshDisabled = false;
}
StateHasChanged();
}
private void 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,207 @@
@page "/logging"
@using ColorControl.Shared.Contracts
@using ColorControl.UI.Services;
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
<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",
_ => "info"
};
public bool Show(NLog.LogLevel level) => LogLevel >= level;
}
protected override async Task OnInitializedAsync()
{
config ??= await _rpcClientService.CallAsync<Config>("OptionsService", "GetConfig");
logLevel = config.LogLevel;
if (allLines == null)
{
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 void LogLevelFilterInput(ChangeEventArgs e)
{
var value = e.Value as string ?? NLog.LogLevel.Debug.Name;
logLevelFilter = NLog.LogLevel.FromString(value);
}
}
@@ -0,0 +1,178 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
<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" @onclick="SetDitherRegistryKeyOnClick">Set dither registry key</button>
<button type="button" class="btn btn-secondary" @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,267 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
@inject JSHelper jsHelper;
<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-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(Preset != null ? Preset.IsDisplayPreset ? $"Driver settings for display {Preset.displayName}" : $"Driver settings for {Preset.IdOrName}" : "Driver settings")</h5>
@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 (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 profile in Profiles)
{
<option value="@profile.Name" title="@string.Join(", ", profile.Apps)">@profile.Name</option>
}
</select>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" @bind="OnlyFoundPrograms" id="onlyFoundPrograms" @bind:after="OnlyFoundProgramsOnChange">
<label class="form-check-label" for="onlyFoundPrograms">
Only show found programs
</label>
</div>
}
<div class="accordion accordion-flush" id="driverSettingsAccordion">
@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)
{
<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)
{
<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" @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<NvProfile>? Profiles { get; set; }
private string? CurrentProfileName { get; set; }
private bool IsVisible { get; set; }
private bool OnlyFoundPrograms = true;
protected override async Task OnParametersSetAsync()
{
if (_preset != Preset)
{
_preset = Preset;
CurrentProfileName = Preset?.DriverProfileName;
DriverSettings = null;
await LoadDriverSettings();
}
}
protected async Task LoadDriverSettings()
{
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);
await jsHelper.CloseModal("driverSettingsModal");
await jsHelper.OpenModal("presetModal");
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 OnlyFoundProgramsOnChange()
{
Profiles = await _rpcClientService.CallAsync<List<NvProfile>>("NvService", "GetDriverProfileNames", OnlyFoundPrograms);
}
private async Task OnFocus(FocusEventArgs e)
{
if (!IsVisible)
{
IsVisible = true;
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" : "";
}
}
@@ -0,0 +1,186 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
@inject JSHelper jsHelper;
<div class="modal modal-lg" id="gpuSettingsModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" @onfocus="OnFocus">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(Preset != null ? $"GPU settings for {Preset.IdOrName}" : $"GPU settings for {GpuInfo?.Name ?? "?"}")</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 (OverclockSettings != null && GpuInfos?.Any() == true)
{
<div class="mb-2">
<label class="form-label" for="gpu">GPU</label>
<select class="form-select" id="gpu" @bind="OverclockSettings.PCIIdentifier">
@foreach (var gpu in GpuInfos)
{
<option value="@gpu.PCIIdentifier">@gpu.Name</option>
}
</select>
</div>
}
@if (OverclockSettings != null && GpuInfo != null)
{
<div class="mb-2">
<label class="form-label" for="ocType">Overclock method</label>
<select class="form-select" id="ocType" @bind="OverclockSettings.Type">
@foreach (var ocType in Enum.GetValues<NvGpuOcType>())
{
<option value="@ocType.ToString()">@ocType.GetDescription()</option>
}
</select>
</div>
@if (OverclockSettings.Type is NvGpuOcType.Curve or NvGpuOcType.Offset)
{
<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)
{
<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)
{
<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)
{
<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
{
<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: "%")}")" />
}
}
}
</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" @onclick="ApplyClick">Save</button>
}
</div>
</div>
</div>
</div>
@code {
[Parameter]
public NvGpuOcSettings? OverclockSettings { get; set; }
[Parameter]
public NvGpuInfoDto? GpuInfo { get; set; }
private NvPreset? _preset;
[Parameter]
public NvPreset? Preset { get; set; }
[Parameter]
public Func<NvGpuInfoDto, Task>? AfterApply { get; set; }
private List<NvGpuInfoDto>? GpuInfos;
private bool IsVisible { get; set; }
protected override async Task OnParametersSetAsync()
{
if (!IsVisible)
{
return;
}
if (_preset != Preset)
{
_preset = Preset;
}
if (GpuInfo == null)
{
GpuInfos = await _rpcClientService.CallAsync<List<NvGpuInfoDto>>("NvService", "GetGpuInfos");
GpuInfo = GpuInfos?.FirstOrDefault();
}
if (Preset != null)
{
OverclockSettings = Preset.ocSettings.FirstOrDefault();
}
if (GpuInfo != null && OverclockSettings == null)
{
OverclockSettings = GpuInfo.OverclockSettings;
}
}
private async Task ApplyClick(MouseEventArgs e)
{
if (OverclockSettings == null)
{
return;
}
if (Preset != null)
{
Preset.UpdateOverclockingSetting(OverclockSettings);
await jsHelper.CloseModal("gpuSettingsModal");
await jsHelper.OpenModal("presetModal");
return;
}
var result = await _rpcClientService.CallAsync<bool>("NvService", "ApplyOverclocking", new List<NvGpuOcSettings> { OverclockSettings });
if (AfterApply != null && GpuInfo != null)
{
await AfterApply.Invoke(GpuInfo);
}
}
private void OnFocus(FocusEventArgs e)
{
if (!IsVisible)
{
IsVisible = true;
}
}
}
@@ -0,0 +1,108 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.UI.Services;
@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;
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,76 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
<div class="modal" id="settingsModal" tabindex="-1" @onfocus="OnFocus">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">NVIDIA settings</h5>
<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 and allow 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,435 @@
@page "/NVIDIA"
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.UI.Pages.Generic
@using ColorControl.UI.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.Shared.Common;
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inherits SummaryComponentBase
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
@inject ProtectedLocalStorage _localStorage
<PageTitle>NVIDIA controller</PageTitle>
<NvidiaSettings />
<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>
<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>
<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">
Settings
</button>
</div>
</div>
<h2>Displays</h2>
<NvidiaPreset PresetParam="CurrentPreset" AfterApply="AfterPresetApplied" SetDriverSettingsPreset="SetDriverSettingsPreset" SetGpuSettingsPreset="SetGpuSettingsPreset" />
<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">
@if (!DisplayPresets.Any())
{
<span>No displays found</span>
}
@foreach (var preset in DisplayPresets)
{
<div class="col-lg-6 mb-3">
<div class="card h-100">
<div class="card-header d-flex align-items-baseline">
<span>
@preset.displayName
@if (preset.DisplayConfig.IsPrimary == true)
{
<span class="ms-2 badge text-bg-primary" title="Primary display">Primary</span>
}
</span>
<div class="d-flex ms-auto">
<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>
<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 />
<small class="text-body">Dithering: @preset.DitherState.GetDescription() @preset.NvDitherBits.GetDescription() @preset.NvDitherMode.GetDescription()</small>
}
@if (preset.HDREnabled)
{
<br />
<small class="text-body">HDR enabled @(preset.HdrSettings.OutputMode.HasValue ? $"({preset.HdrSettings.DisplayOutputMode.GetDescription()})" : ""), SDR brightness: @preset.SDRBrightness%</small>
}
@if (!string.IsNullOrEmpty(preset.ColorProfileSettings.ProfileName))
{
<br />
<small class="text-body">Color profile: @preset.ColorProfileSettings.ProfileName</small>
}
@if (preset.driverSettings?.Any() == true)
{
<br />
@RenderPresetSettings(preset)
}
</div>
</div>
</div>
}
</div>
}
<h2>Display Adapters</h2>
@if (GpuInfos == null)
{
<div>Loading...</div>
}
else
{
<div class="row mb-3">
@if (!GpuInfos.Any())
{
<span>No display adapters found</span>
}
@foreach (var gpuInfo in GpuInfos)
{
<div class="col-lg-6 mb-3">
<div class="card h-100">
<div class="card-header d-flex align-items-baseline">
<span>
@gpuInfo.Name
</span>
@if (NvConfig?.ShowOverclocking == true)
{
<div class="d-flex ms-auto">
<button type="button" class="btn btn-sm btn-primary ms-2" data-bs-toggle="modal" data-bs-target="#gpuSettingsModal" @onclick="() => GpuSettingsOnClick(gpuInfo)">
Settings
</button>
</div>
}
</div>
<div class="card-body">
<small class="text-body">GPU: @gpuInfo.GpuFrequencyInKhz.ToKiloUnitString() @@ @gpuInfo.VoltageInMv mV, @Utils.ToUnitString(values: [gpuInfo.GpuTemperature, gpuInfo.HotSpotTemperature])</small>
<small class="text-body">Memory: @gpuInfo.GpuFrequencyInKhz.ToKiloUnitString(), @Utils.ToUnitString(values: [gpuInfo.MemoryTemperature])</small>
<small class="text-body">Power: @(gpuInfo.PowerInWatts)W</small>
<small class="text-body"><br/>Overclock info: @gpuInfo.OverclockSettings</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">
@if (!Presets.Any())
{
<span>No presets found</span>
}
<div class="list-group">
@foreach (var preset in Presets)
{
<span class="list-group-item list-group-item-action" @onclick="() => PresetOnClick(preset)" @onclick:preventDefault @onclick:stopPropagation>
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="mb-1">
@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 class="d-none d-lg-block">
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Delete</button>
<button class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Clone</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Edit</button>
</span>
<div class="d-lg-none" @onclick:preventDefault @onclick:stopPropagation>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)">Delete</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)">Clone</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)">Edit</a></li>
</ul>
</div>
</div>
<small>
<ul class="list-group list-group-horizontal-md mb-1">
@if (preset.DisplayConfig.ApplyResolution || preset.DisplayConfig.ApplyRefreshRate)
{
<li class="list-group-item p-1">@preset.DisplayConfig.Resolution.ToString() @@ @preset.DisplayConfig.RefreshRate.ToString()Hz</li>
}
@if (preset.applyColorData)
{
<li class="list-group-item p-1">Color settings: @preset.colorData</li>
}
@if (preset.applyDithering)
{
<li class="list-group-item p-1">Dithering: @preset.GetDitheringDescription()</li>
}
@if (preset.ApplyColorEnhancements)
{
<li class="list-group-item p-1">@preset.ColorEnhancementSettings</li>
}
@if (preset.applyHDR)
{
<li class="list-group-item p-1">HDR: @preset.GetHdrDescription()</li>
}
@if (preset.applyDriverSettings)
{
<li class="list-group-item p-1">@RenderPresetSettings(preset)</li>
}
@if (preset.ApplyNovideoSettings)
{
<li class="list-group-item p-1">@preset.NovideoSettings</li>
}
@if (preset.applyHdmiSettings)
{
<li class="list-group-item p-1">@preset.GetHdmiSettingsDescription()</li>
}
@if (preset.applyOverclocking)
{
<li class="list-group-item p-1">@preset.GetOverclockingSettingsDescription()</li>
}
@if (preset.applyOther && !string.IsNullOrEmpty(preset.ColorProfileSettings.ProfileName))
{
<li class="list-group-item p-1">Color profile: @preset.ColorProfileSettings.ProfileName</li>
}
@if (preset.DdcSettings.ApplyDdc && preset.DdcSettings.Settings.Count > 0)
{
<li class="list-group-item p-1">DDC: @preset.DdcSettings.Settings[0]</li>
}
</ul>
</small>
<small class="text-muted">@preset.shortcut</small>
</span>
}
</div>
</div>
</div>
}
@code {
private List<NvPreset>? Presets;
private List<NvPreset>? DisplayPresets;
private List<NvGpuInfoDto>? GpuInfos;
private NvServiceConfig? NvConfig;
private NvPreset? CurrentPreset;
private NvPreset? CurrentDriverSettingsPreset;
private NvPreset? CurrentGpuSettingsPreset;
private NvGpuInfoDto? CurrentGpuInfo;
private bool DitheringVisible;
private bool InfoVisible;
private List<NvSettingItemDto>? DriverSettings;
private PresetOrder PresetOrder;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await Refresh();
}
protected override async Task Refresh()
{
NvConfig = await _rpcClientService.CallAsync<NvServiceConfig>("NvService", "GetConfig");
await RefreshPresets();
DisplayPresets = await _rpcClientService.CallAsync<List<NvPreset>>("NvService", "GetDisplayPresets");
GpuInfos = await _rpcClientService.CallAsync<List<NvGpuInfoDto>>("NvService", "GetGpuInfos");
}
protected override async Task OnAfterRenderAsync(bool firstTime)
{
if (firstTime)
{
PresetOrder = await _localStorage.TryGet<PresetOrder>("NvidiaPresetOrder");
Presets = Presets?.OrderPresetsBy(PresetOrder);
StateHasChanged();
}
}
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)
{
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)
{
CurrentPreset = preset;
}
private void PresetOnCloneClick(NvPreset preset)
{
CurrentPreset = new NvPreset(preset);
CurrentPreset.name = preset.name;
CurrentPreset.SetCloneValues();
}
private void 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 void DisplayPresetOnClick(NvPreset preset)
{
CurrentPreset = preset;
}
private void 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 Task SetDriverSettingsPreset(NvPreset preset)
{
CurrentDriverSettingsPreset = preset;
StateHasChanged();
return Task.CompletedTask;
}
private Task SetGpuSettingsPreset(NvPreset preset)
{
CurrentGpuSettingsPreset = preset;
StateHasChanged();
return Task.CompletedTask;
}
private void GpuSettingsOnClick(NvGpuInfoDto gpuInfo)
{
CurrentGpuInfo = gpuInfo;
CurrentGpuSettingsPreset = null;
}
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);
}
private async Task DriverSettingsOnClick(NvPreset preset)
{
DriverSettings ??= await _rpcClientService.CallAsync<List<NvSettingItemDto>>("NvService", "GetDriverSettings", Type.Missing, true);
}
}
@@ -0,0 +1,373 @@
@page "/options"
@using ColorControl.Shared.Contracts
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
@inject AppState AppState
@inject NavigationManager _navigationManager
@inject NotificationService _notificationService
@inject JSHelper _jsHelper;
@inject IHttpContextAccessor _httpContextAccessor
<PageTitle>Options</PageTitle>
<h1>Options</h1>
@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="config.ElevationMethod" id="elevationMethod" @oninput="ElevationMethodInput" aria-describedby="elevationMethodHelp">
@foreach (var method in Enum.GetValues<ElevationMethod>())
{
<option value="@method">@method.GetDescription()</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 (use 0 for automatic assignment)
</label>
@if (IsChangingUiPort)
{
<div class="alert alert-warning mb-2" role="alert">
Changing the UI port. If the UI is not reloaded correctly, close this window and relaunch the UI.
</div>
}
<input class="form-control" type="number" @bind="config.UiPort" id="webUiPort" @bind:after="UiPortBlur">
@if (config.CurrentUiPort > 0)
{
<div id="uiPortHelp" class="form-text">Currently listening on port @config.CurrentUiPort</div>
}
</div>
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" @bind="config.UiAllowRemoteConnections" id="uiAllowRemoteConnections" @bind:after="UiAllowRemoteConnectionsChange">
<label class="form-check-label" for="uiAllowRemoteConnections">
Web UI: Allow remote connections
</label>
<div id="uiAllowRemoteConnectionsHelp" class="form-text">Allows or disallows remote connections to the UI. Only enable this if you want to access the UI from another device (eg. your phone) than the system ColorControl is running on</div>
</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>
<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">
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>
}
@code {
private Config? config;
private bool IsEmbeddedBrowser;
private bool IsChangingUiPort;
protected override async Task OnInitializedAsync()
{
config = await _rpcClientService.CallAsync<Config>("OptionsService", "GetConfig");
IsEmbeddedBrowser = _httpContextAccessor.HttpContext?.Request.Headers["user-agent"].ToString().Contains("ColorControlEmbedded") == true;
}
private async Task SaveConfig()
{
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);
}
else if (module.IsActive)
{
_notificationService.SendNotification(new NotificationDto("Cannot activate the module: check the log for more info", Constants.Danger) { InternalUrl = "/logging" });
module.IsActive = false;
}
}
private async Task PollingIntervalOnInput()
{
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 ? Enum.Parse<ElevationMethod>((string)e.Value) : ElevationMethod.None;
await _rpcClientService.CallAsync<bool>("OptionsService", "SetElevationMethod", 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()
{
if (config == null)
{
return;
}
IsChangingUiPort = true;
if (config.UiPort > 0 || IsEmbeddedBrowser)
{
await NavigateToNewPort(config.UiPort, 5000);
}
var result = await _rpcClientService.CallAsync<bool>("OptionsService", "SetUiPort", config.UiPort);
}
private async Task UiAllowRemoteConnectionsChange()
{
await _rpcClientService.CallAsync<bool>("OptionsService", "SetUiAllowRemoteConnections", config!.UiAllowRemoteConnections);
}
private async Task NavigateToNewPort(int port, int timeout = 0)
{
var uri = new UriBuilder(_navigationManager.Uri);
uri.Port = port;
var newUri = uri.Uri.ToString();
if (timeout == 0)
{
_navigationManager.NavigateTo(uri.ToString(), true);
}
else
{
await _jsHelper.NavigateTo(newUri, timeout);
}
}
}
@@ -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,318 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.Base
@using ColorControl.Shared.Contracts.RemoteControl
@using ColorControl.UI.Services
@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;
}
}
@@ -0,0 +1,264 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Services
@using ColorControl.Shared.Common
@rendermode Constants.RenderMode
@inject RpcUiClientService _rpcClientService
<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">
<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 py-2" 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="checkPowerOnAfterSessionUnlock" @bind="Device.Options.PowerOnAfterSessionUnlock">
<label class="form-check-label" for="checkPowerOnAfterSessionUnlock">Power on when unlocking the pc</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="checkPowerOffAfterSessionLock" @bind="Device.Options.PowerOffAfterSessionLock">
<label class="form-check-label" for="checkPowerOffAfterSessionLock">Power off when locking the pc</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">
@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>
</div>
</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;
[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 void OnParametersSet()
{
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 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;
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,170 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.UI.Services
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.Shared.Common
@using static ColorControl.UI.Components.Pages.Generic.StepsInput<ColorControl.Shared.Contracts.Samsung.SamsungPreset>
@rendermode Constants.RenderMode
@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-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
{
<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 void OnParametersSet()
{
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,255 @@
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode Constants.RenderMode
@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 void OnInitialized()
{
// 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 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();
return Task.CompletedTask;
}
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. You can also manually enter the unlock codes 0098 (for TV's) and 0038 (for monitors) while the 'Advanced' item is highlighted."),
AddField("Advanced Unlock Type", AdvancedUnlockType.TV, subLabel: "Select the desired unlock type for the Advanced section. If the target is a TV choose TV, otherwise choose Monitor")
];
StateHasChanged();
}
protected 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();
return Task.CompletedTask;
}
protected async Task FactoryMenuSecondStepEnter()
{
if (Fields == null)
{
return;
}
var unlockType = Fields.First(f => f.ValueType == typeof(AdvancedUnlockType)).ValueAsEnum<AdvancedUnlockType>();
if (unlockType == AdvancedUnlockType.TV)
{
await ExecutePreset(SamsungFactoryMenuPresets.PresetAdvanced);
}
else
{
await ExecutePreset(SamsungFactoryMenuPresets.PresetAdvancedMonitor);
}
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.UI.Services;
@using ColorControl.Shared.Common;
@using ColorControl.UI.Components.Pages.Generic
@rendermode Constants.RenderMode
@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,371 @@
@page "/Samsung"
@using ColorControl.Shared.Common
@using ColorControl.Shared.Contracts
@using ColorControl.Shared.Contracts.NVIDIA
@using ColorControl.Shared.Contracts.Samsung
@using ColorControl.UI.Components.Pages.RemoteControl
@using ColorControl.UI.Pages.Generic
@using ColorControl.UI.Services;
@using ColorControl.UI.Components.Pages.Generic
@using ColorControl.UI.Generics
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inherits SummaryComponentBase
@rendermode Constants.RenderMode
@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">
<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
</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>
<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" />
@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="#samsungDeviceModal" @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="#samsungDeviceModal" @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">
@if (!Presets.Any())
{
<span>No presets found</span>
}
<div class="list-group">
@foreach (var preset in Presets)
{
<span class="list-group-item list-group-item-action" @onclick="() => PresetOnClick(preset)" @onclick:preventDefault @onclick:stopPropagation>
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="mb-1">@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 class="d-none d-lg-block">
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Delete</button>
<button class="btn btn-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Clone</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)" @onclick:preventDefault @onclick:stopPropagation>Edit</button>
</span>
<div class="d-lg-none" @onclick:preventDefault @onclick:stopPropagation>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#confirmModal" @onclick="() => PresetOnDeleteClick(preset)">Delete</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnCloneClick(preset)">Clone</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#presetModal" @onclick="() => PresetOnEditClick(preset)">Edit</a></li>
</ul>
</div>
</div>
</span>
}
</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()
{
await base.OnInitializedAsync();
await Refresh();
}
protected override async Task Refresh()
{
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");
}
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 void PresetOnEditClick(SamsungPreset preset)
{
CurrentPreset = preset;
}
private void PresetOnCloneClick(SamsungPreset preset)
{
CurrentPreset = new SamsungPreset(preset);
CurrentPreset.SetCloneValues();
}
private void 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 void DeviceOnClick(SamsungDeviceDto device)
{
CurrentDevice = device;
}
private void AddDeviceOnClick()
{
CurrentDevice = new SamsungDeviceDto { IsCustom = true };
}
private void 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);
}
}
+6
View File
@@ -0,0 +1,6 @@
<Router AppAssembly="@typeof(Blazor).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
</Router>
+10
View File
@@ -0,0 +1,10 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using ColorControl.UI
@using ColorControl.UI.Components
+16
View File
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
public static class Constants
{
public const string Primary = "primary";
public const string Secondary = "secondary";
public const string Success = "success";
public const string Danger = "danger";
public const string Warning = "warning";
public const string Info = "info";
public const string Light = "light";
public const string Dark = "dark";
public static IComponentRenderMode RenderMode = new InteractiveServerRenderMode(prerender: false);
}
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using System.Security.Cryptography;
namespace ColorControl.UI.Generics;
public static class GeneralExtensions
{
public static async ValueTask<TValue?> TryGet<TValue>(this ProtectedBrowserStorage storage, string key, TValue? def = default)
{
try
{
var result = await storage.GetAsync<TValue>(key);
var value = result.Success ? result.Value : def;
return value;
}
catch (CryptographicException)
{
await storage.DeleteAsync(key);
return def;
}
}
}
@@ -0,0 +1,25 @@
using ColorControl.Shared.Contracts;
namespace ColorControl.UI.Generics;
public static class PresetExtensions
{
public static List<T>? OrderPresetsBy<T>(this List<T> presets, PresetOrder order) where T : PresetBase
{
if (presets == null)
{
return null;
}
if (order == PresetOrder.ByName)
{
presets = presets.OrderBy(p => p.name).ToList();
}
else if (order == PresetOrder.ByLastUsed)
{
presets = presets.OrderByDescending(p => p.LastUsed).ToList();
}
return presets;
}
}
+26
View File
@@ -0,0 +1,26 @@
using NStandard;
namespace ColorControl.UI;
public class NotificationDto
{
public long Id { get; }
public string Message { get; }
public string Level { get; } = Constants.Info;
public DateTime? ScheduledAt { get; set; }
public string? InternalUrl { get; set; }
public DateTime? ShownAt { get; set; }
public NotificationDto(string message, string level = Constants.Info)
{
Message = message;
Level = level;
Id = Random.Shared.NextInt64();
}
public string Category => Level switch
{
Constants.Danger => "Error",
_ => Level[0].ToUpper() + Level.Substring(1)
};
}
+20
View File
@@ -0,0 +1,20 @@
using ColorControl.Shared.Contracts;
using ColorControl.UI;
using ColorControl.UI.Services;
using System.Diagnostics;
var arguments = args;
if (Debugger.IsAttached)
{
var rpcServer = new RpcUiClientService(null);
var options = new RpcUiClientOptions
{
Timeout = 10000
};
var config = await rpcServer.CallWithOptionsAsync<Config>("OptionsService", "GetConfig", options);
arguments = ["", config.UseDarkMode.ToString(), config.UiPort.ToString(), config.UiAllowRemoteConnections.ToString()];
}
await Blazor.Start(arguments);
@@ -0,0 +1,23 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5175",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7135;http://localhost:5175",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+79
View File
@@ -0,0 +1,79 @@
using Microsoft.JSInterop;
namespace ColorControl.UI.Services;
public class RectDimension
{
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
public double Height { get; set; }
}
public class JSHelper(IJSRuntime jsRuntime)
{
private IJSObjectReference? jsModule;
public async Task<bool> IsFormValid(string id)
{
return await InvokeAsync<bool>("FormValid", id);
}
public async Task OpenModal(string id)
{
await InvokeVoidAsync("OpenModal", id);
}
public async Task CloseModal(string id)
{
await InvokeVoidAsync("CloseModal", id);
}
public async Task<bool> IsModalShown()
{
return await InvokeAsync<bool>("IsModalShown");
}
public async Task ShowToast(string id)
{
await InvokeVoidAsync("ShowToast", id);
}
public async Task NavigateTo(string uri, int timeout = 0)
{
await InvokeVoidAsync("NavigateTo", uri, timeout);
}
public async Task<RectDimension> GetElementDimensions(string id)
{
jsModule = await LoadModule();
return await jsModule.InvokeAsync<RectDimension>("GetElementDimensions", id);
}
public async Task<string> GetElementClassName(string id)
{
jsModule = await LoadModule();
return await jsModule.InvokeAsync<string>("GetElementClassName", id);
}
private async ValueTask<T> InvokeAsync<T>(string identifier, params object?[]? names) where T : struct
{
jsModule = await LoadModule();
return await jsModule.InvokeAsync<T>(identifier, names);
}
private async ValueTask InvokeVoidAsync(string identifier, params object?[]? names)
{
jsModule = await LoadModule();
await jsModule.InvokeVoidAsync(identifier, names);
}
private async Task<IJSObjectReference> LoadModule()
{
return jsModule ??= await jsRuntime.InvokeAsync<IJSObjectReference>("import", "./helper.js");
}
}
@@ -0,0 +1,34 @@
namespace ColorControl.UI.Services;
public class NotificationService
{
public event Func<NotificationDto, Task>? OnNotification;
private List<NotificationDto> _scheduledNotifications = new();
public void SendNotification(NotificationDto notification)
{
var now = DateTime.UtcNow;
if (notification.ScheduledAt == null || notification.ScheduledAt <= now)
{
OnNotification?.Invoke(notification);
return;
}
Task.Run(async () =>
{
await Task.Delay(notification.ScheduledAt.Value - now);
OnNotification?.Invoke(notification);
});
}
public async Task SendNotificationDirect(NotificationDto notification)
{
if (OnNotification != null)
{
await OnNotification.Invoke(notification);
}
}
}
@@ -0,0 +1,102 @@
using ColorControl.Shared.Common;
using ColorControl.Shared.Contracts;
using Newtonsoft.Json;
using System.Diagnostics;
namespace ColorControl.UI.Services;
public class RpcUiClientOptions
{
public int Timeout { get; set; } = PipeUtils.DefaultTimeout;
}
public class RpcUiClientService(NotificationService? notificationService)
{
public string? LastErrorMessage { get; private set; }
public async Task<T> CallAsync<T>(string serviceName, string method, params object?[] arguments)
{
var rpcMessage = new SvcRpcMessage
{
ServiceName = serviceName,
MethodName = method,
Arguments = arguments
};
var timeout = Debugger.IsAttached ? 60000 : PipeUtils.DefaultTimeout;
return await SendRpcMessageAsync<T>(rpcMessage, timeout, pipeName: PipeUtils.MainPipe);
}
public async Task<bool> CallGenericBoolResultAsync(string serviceName, string method, params object?[] arguments)
{
var rpcMessage = new SvcRpcMessage
{
ServiceName = serviceName,
MethodName = method,
Arguments = arguments
};
var timeout = Debugger.IsAttached ? 60000 : PipeUtils.DefaultTimeout;
var result = await SendRpcMessageAsync<GenericBoolResult>(rpcMessage, timeout, pipeName: PipeUtils.MainPipe);
if (result?.Result == true)
{
return true;
}
if (result?.ErrorMessages.Count > 0 && notificationService != null)
{
await notificationService.SendNotificationDirect(new NotificationDto(result.ErrorMessages.First(), Constants.Danger));
}
return false;
}
public async Task<T> CallWithOptionsAsync<T>(string serviceName, string method, RpcUiClientOptions options, params object?[] arguments)
{
var rpcMessage = new SvcRpcMessage
{
ServiceName = serviceName,
MethodName = method,
Arguments = arguments
};
return await SendRpcMessageAsync<T>(rpcMessage, options.Timeout, pipeName: PipeUtils.MainPipe);
}
private async Task<T> SendRpcMessageAsync<T>(SvcRpcMessage message, int timeout = PipeUtils.DefaultTimeout, string pipeName = PipeUtils.ServicePipe)
{
var messageJson = JsonConvert.SerializeObject(message);
var resultJson = await PipeUtils.SendMessageAsync(messageJson, timeout, pipeName);
if (resultJson == null)
{
notificationService?.SendNotification(new NotificationDto("Unknown communication error", Constants.Danger));
return default;
}
var resultMessage = JsonConvert.DeserializeObject<SvcResultMessage>(resultJson);
if (resultMessage?.Data == null)
{
LastErrorMessage = resultMessage?.ErrorMessage ?? "Unknown communication error";
if (notificationService != null)
{
await notificationService.SendNotificationDirect(new NotificationDto(LastErrorMessage, Constants.Danger));
}
return default;
}
LastErrorMessage = null;
var result = JsonConvert.DeserializeObject<T>(resultMessage.Data);
return result;
}
}
+16
View File
@@ -0,0 +1,16 @@
namespace ColorControl.UI;
public class AppState
{
public string SelectedTheme { get; set; } = "light";
public event Action? OnChange;
public void SetTheme(string theme)
{
SelectedTheme = theme;
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}
@@ -0,0 +1,9 @@
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
+55
View File
@@ -0,0 +1,55 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}
.dropdown-item, .list-group-item {
cursor: pointer;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+33
View File
@@ -0,0 +1,33 @@
export function FormValid(formId) {
let form = document.getElementById(formId);
return form.checkValidity();
}
export function CloseModal(modalId) {
bootstrap.Modal.getOrCreateInstance('#' + modalId)?.hide();
}
export function OpenModal(modalId) {
bootstrap.Modal.getOrCreateInstance('#' + modalId)?.show();
}
export function ShowToast(toastId) {
const toastBootstrap = bootstrap.Toast.getOrCreateInstance('#' + toastId);
toastBootstrap?.show();
}
export function GetElementDimensions(elementId) {
return document.getElementById(elementId).getBoundingClientRect();
}
export function GetElementClassName(elementId) {
return document.getElementById(elementId).className;
}
export function NavigateTo(uri, timeout = 0) {
setTimeout(() => window.location = uri, timeout);
}
export function IsModalShown() {
return document.getElementsByClassName("modal show").length > 0;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

-101
View File
@@ -1,101 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32014.148
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ColorControl", "ColorControl\ColorControl.csproj", "{A6005D08-70F0-44CB-A384-9219EBA84F21}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nspector", "Nspector\Nspector.csproj", "{76797290-48F5-4A82-86C3-FAD25DAE4500}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NvAPIWrapper", "NvAPIWrapper\NvAPIWrapper.csproj", "{42E2B4A6-6857-4427-AEF1-DF4C7EF8CC7C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "novideo_srgb", "novideo_srgb\novideo_srgb.csproj", "{A6A97834-7BE1-474A-B92F-A512DB1D5186}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NLogViewer", "NLogViewer\NLogViewer.csproj", "{D5E4D314-CFCD-4859-98FC-68F660AC7FB4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "Shared\Shared.csproj", "{8F38D6B0-82DA-4DD1-89EB-918D8A18AEB1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LittleCms", "LittleCms\LittleCms.csproj", "{42D251AE-42AF-4C0D-B163-7C7A13C4A9A6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MHC2Gen", "MHC2Gen\MHC2Gen.csproj", "{9FDF741F-0ECF-46DB-8671-AB11ACEB9F41}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A6005D08-70F0-44CB-A384-9219EBA84F21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A6005D08-70F0-44CB-A384-9219EBA84F21}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A6005D08-70F0-44CB-A384-9219EBA84F21}.Debug|x64.ActiveCfg = Debug|Any CPU
{A6005D08-70F0-44CB-A384-9219EBA84F21}.Debug|x64.Build.0 = Debug|Any CPU
{A6005D08-70F0-44CB-A384-9219EBA84F21}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A6005D08-70F0-44CB-A384-9219EBA84F21}.Release|Any CPU.Build.0 = Release|Any CPU
{A6005D08-70F0-44CB-A384-9219EBA84F21}.Release|x64.ActiveCfg = Release|Any CPU
{A6005D08-70F0-44CB-A384-9219EBA84F21}.Release|x64.Build.0 = Release|Any CPU
{76797290-48F5-4A82-86C3-FAD25DAE4500}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76797290-48F5-4A82-86C3-FAD25DAE4500}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76797290-48F5-4A82-86C3-FAD25DAE4500}.Debug|x64.ActiveCfg = Debug|Any CPU
{76797290-48F5-4A82-86C3-FAD25DAE4500}.Debug|x64.Build.0 = Debug|Any CPU
{76797290-48F5-4A82-86C3-FAD25DAE4500}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76797290-48F5-4A82-86C3-FAD25DAE4500}.Release|Any CPU.Build.0 = Release|Any CPU
{76797290-48F5-4A82-86C3-FAD25DAE4500}.Release|x64.ActiveCfg = Release|Any CPU
{76797290-48F5-4A82-86C3-FAD25DAE4500}.Release|x64.Build.0 = Release|Any CPU
{42E2B4A6-6857-4427-AEF1-DF4C7EF8CC7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42E2B4A6-6857-4427-AEF1-DF4C7EF8CC7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42E2B4A6-6857-4427-AEF1-DF4C7EF8CC7C}.Debug|x64.ActiveCfg = Debug|Any CPU
{42E2B4A6-6857-4427-AEF1-DF4C7EF8CC7C}.Debug|x64.Build.0 = Debug|Any CPU
{42E2B4A6-6857-4427-AEF1-DF4C7EF8CC7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42E2B4A6-6857-4427-AEF1-DF4C7EF8CC7C}.Release|Any CPU.Build.0 = Release|Any CPU
{42E2B4A6-6857-4427-AEF1-DF4C7EF8CC7C}.Release|x64.ActiveCfg = Release|Any CPU
{42E2B4A6-6857-4427-AEF1-DF4C7EF8CC7C}.Release|x64.Build.0 = Release|Any CPU
{A6A97834-7BE1-474A-B92F-A512DB1D5186}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A6A97834-7BE1-474A-B92F-A512DB1D5186}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A6A97834-7BE1-474A-B92F-A512DB1D5186}.Debug|x64.ActiveCfg = Debug|Any CPU
{A6A97834-7BE1-474A-B92F-A512DB1D5186}.Debug|x64.Build.0 = Debug|Any CPU
{A6A97834-7BE1-474A-B92F-A512DB1D5186}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A6A97834-7BE1-474A-B92F-A512DB1D5186}.Release|Any CPU.Build.0 = Release|Any CPU
{A6A97834-7BE1-474A-B92F-A512DB1D5186}.Release|x64.ActiveCfg = Release|Any CPU
{A6A97834-7BE1-474A-B92F-A512DB1D5186}.Release|x64.Build.0 = Release|Any CPU
{D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Debug|x64.ActiveCfg = Debug|Any CPU
{D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Debug|x64.Build.0 = Debug|Any CPU
{D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Release|Any CPU.Build.0 = Release|Any CPU
{D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Release|x64.ActiveCfg = Release|Any CPU
{D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Release|x64.Build.0 = Release|Any CPU
{8F38D6B0-82DA-4DD1-89EB-918D8A18AEB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8F38D6B0-82DA-4DD1-89EB-918D8A18AEB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F38D6B0-82DA-4DD1-89EB-918D8A18AEB1}.Debug|x64.ActiveCfg = Debug|Any CPU
{8F38D6B0-82DA-4DD1-89EB-918D8A18AEB1}.Debug|x64.Build.0 = Debug|Any CPU
{8F38D6B0-82DA-4DD1-89EB-918D8A18AEB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F38D6B0-82DA-4DD1-89EB-918D8A18AEB1}.Release|Any CPU.Build.0 = Release|Any CPU
{8F38D6B0-82DA-4DD1-89EB-918D8A18AEB1}.Release|x64.ActiveCfg = Release|Any CPU
{8F38D6B0-82DA-4DD1-89EB-918D8A18AEB1}.Release|x64.Build.0 = Release|Any CPU
{42D251AE-42AF-4C0D-B163-7C7A13C4A9A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42D251AE-42AF-4C0D-B163-7C7A13C4A9A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42D251AE-42AF-4C0D-B163-7C7A13C4A9A6}.Debug|x64.ActiveCfg = Debug|Any CPU
{42D251AE-42AF-4C0D-B163-7C7A13C4A9A6}.Debug|x64.Build.0 = Debug|Any CPU
{42D251AE-42AF-4C0D-B163-7C7A13C4A9A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42D251AE-42AF-4C0D-B163-7C7A13C4A9A6}.Release|Any CPU.Build.0 = Release|Any CPU
{42D251AE-42AF-4C0D-B163-7C7A13C4A9A6}.Release|x64.ActiveCfg = Release|Any CPU
{42D251AE-42AF-4C0D-B163-7C7A13C4A9A6}.Release|x64.Build.0 = Release|Any CPU
{9FDF741F-0ECF-46DB-8671-AB11ACEB9F41}.Debug|Any CPU.ActiveCfg = Debug|x64
{9FDF741F-0ECF-46DB-8671-AB11ACEB9F41}.Debug|Any CPU.Build.0 = Debug|x64
{9FDF741F-0ECF-46DB-8671-AB11ACEB9F41}.Debug|x64.ActiveCfg = Debug|x64
{9FDF741F-0ECF-46DB-8671-AB11ACEB9F41}.Debug|x64.Build.0 = Debug|x64
{9FDF741F-0ECF-46DB-8671-AB11ACEB9F41}.Release|Any CPU.ActiveCfg = Release|x64
{9FDF741F-0ECF-46DB-8671-AB11ACEB9F41}.Release|Any CPU.Build.0 = Release|x64
{9FDF741F-0ECF-46DB-8671-AB11ACEB9F41}.Release|x64.ActiveCfg = Release|x64
{9FDF741F-0ECF-46DB-8671-AB11ACEB9F41}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {34BA158F-AE57-4A7B-A372-11FEFBBC1F98}
EndGlobalSection
EndGlobal
+17
View File
@@ -0,0 +1,17 @@
<Solution>
<Configurations>
<Platform Name="Any CPU" />
<Platform Name="x64" />
</Configurations>
<Project Path="ColorControl.UI/ColorControl.UI.csproj" />
<Project Path="ColorControl/ColorControl.csproj" />
<Project Path="LittleCms/LittleCms.csproj" />
<Project Path="MHC2Gen/MHC2Gen.csproj">
<Platform Project="x64" />
</Project>
<Project Path="NLogViewer/NLogViewer.csproj" />
<Project Path="novideo_srgb/novideo_srgb.csproj" />
<Project Path="Nspector/Nspector.csproj" />
<Project Path="NvAPIWrapper/NvAPIWrapper.csproj" />
<Project Path="Shared/Shared.csproj" />
</Solution>
+111
View File
@@ -0,0 +1,111 @@
using ColorControl.Shared.Common;
using ColorControl.Shared.Contracts;
using ColorControl.Shared.Services;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace ColorControl;
public static class BlazorUiManager
{
private const string ImageName = "ColorControl.UI.exe";
private const string ProcessName = "ColorControl.UI";
private const bool DebugUi = true;
private static bool FirsTime = true;
private static Mutex UiMutex;
private static string MutexId;
public static bool IsRunning(Config config)
{
return Process.GetProcessesByName(ProcessName).Length > 0 &&
(config.UiPort == 0 || config.CurrentUiPort == config.UiPort) &&
config.CurrentUiAllowRemoteConnections == config.UiAllowRemoteConnections;
}
public static async Task<bool> Start(Config config)
{
if ((!FirsTime || Debugger.IsAttached && DebugUi) && IsRunning(config))
{
return false;
}
FirsTime = false;
await Stop();
var uiExe = Path.Combine(Environment.CurrentDirectory, ImageName);
if (!File.Exists(uiExe))
{
uiExe = Path.Combine(Environment.CurrentDirectory.Replace("ColorControl\\bin", "ColorControl.UI\\bin"), ImageName);
if (!File.Exists(uiExe))
{
throw new InvalidOperationException("UI executable not found");
}
}
MutexId = $"Global\\{ProcessName}_{Guid.NewGuid()}";
Utils.ExecuteOnMainThread(() =>
{
UiMutex = new Mutex(false, MutexId);
UiMutex.WaitOne();
});
var windowStyle = ProcessWindowStyle.Hidden;
if (Debugger.IsAttached)
{
windowStyle = ProcessWindowStyle.Normal;
}
var processStartInfo = new ProcessStartInfo(uiExe, [MutexId, config.UseDarkMode.ToString(), config.UiPort.ToString(), config.UiAllowRemoteConnections.ToString()])
{
WindowStyle = windowStyle
};
Process.Start(processStartInfo);
return true;
}
public static async Task Stop()
{
if (UiMutex != null)
{
Utils.ExecuteOnMainThread(() =>
{
UiMutex.ReleaseMutex();
UiMutex.Close();
UiMutex = null;
});
return;
}
var processes = Process.GetProcessesByName(ProcessName);
if (processes.Length <= 0)
{
return;
}
var winApi = Program.ServiceProvider.GetRequiredService<WinApiAdminService>();
winApi.StopProcessByName(ProcessName);
}
public static string GetCurrentUrl(Config config)
{
var port = GetCurrentPort(config);
return $"http://localhost:{port}";
}
public static int GetCurrentPort(Config config = null)
{
return config?.CurrentUiPort ?? -1;
}
}
+39 -37
View File
@@ -1,25 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows10.0.20348.0</TargetFramework>
<TargetFramework>net10.0-windows</TargetFramework>
<OutputType>WinExe</OutputType>
<IsWebBootstrapper>false</IsWebBootstrapper>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>false</MapFileExtensions>
<UpdateUrl>https://www.maassoft.com/downloads/ColorControl/</UpdateUrl>
<ProductName>ColorControl</ProductName>
<PublisherName>Maassoft</PublisherName>
<Company>Maassoft</Company>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>9.8.0.0</ApplicationVersion>
<Version>9.8.0.0</Version>
<ApplicationVersion>11.0.0.0</ApplicationVersion>
<Version>11.0.0.0</Version>
<UseApplicationTrust>false</UseApplicationTrust>
<PublishWizardCompleted>true</PublishWizardCompleted>
<BootstrapperEnabled>false</BootstrapperEnabled>
@@ -27,18 +20,11 @@
<UseWindowsForms>true</UseWindowsForms>
<UseWPF>false</UseWPF>
<ImportWindowsDesktopTargets>true</ImportWindowsDesktopTargets>
<ApplicationIcon>pngbarn.ico</ApplicationIcon>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
<Copyright>2020-2024 Maassoft</Copyright>
<ApplicationIcon>Resources\pngbarn.ico</ApplicationIcon>
<Copyright>2020-2025 Maassoft</Copyright>
<Authors>Maassoft</Authors>
<ManifestCertificateThumbprint>CF0BC153EA664CD97AC6F234B91F45D34630958F</ManifestCertificateThumbprint>
<ManifestKeyFile>ColorControl_1_TemporaryKey.pfx</ManifestKeyFile>
<GenerateManifests>true</GenerateManifests>
<TargetZone>LocalIntranet</TargetZone>
<ApplicationManifest>Properties\app.manifest</ApplicationManifest>
<SignManifests>true</SignManifests>
<AssemblyOriginatorKeyFile>ColorControl_TemporaryKey.pfx</AssemblyOriginatorKeyFile>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<OutputPath>bin\x64\Debug\</OutputPath>
@@ -50,18 +36,28 @@
</PropertyGroup>
<PropertyGroup>
<PathMap Condition="'$(Configuration)' == 'Release'">$(MSBuildThisFileDirectory)=.\</PathMap>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\LgRemotes.json" />
<None Remove="Resources\SamsungRemotes.json" />
<None Remove="XForms\BrowserWindow.xaml" />
<None Remove="XForms\ColorProfileWindow.xaml" />
<None Remove="XForms\LogWindow.xaml" />
</ItemGroup>
<ItemGroup>
<Compile Update="Forms\ElevatedForm.cs">
<SubType>Form</SubType>
<Compile Update="Forms\OptionsPanel.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Update="Forms\InfoPanel.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Update="Properties\Resources.Designer.cs">
<Generator></Generator>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Services\Common\QuickAccessForm.cs">
<SubType>Form</SubType>
</Compile>
@@ -75,16 +71,17 @@
<Compile Update="Services\Samsung\SamsungPanel.cs">
<SubType>UserControl</SubType>
</Compile>
<EmbeddedResource Include="Resources\LgRemotes.json" />
<EmbeddedResource Include="Resources\LG_register_legacy.json" />
<EmbeddedResource Include="Resources\LG_register.json" />
<EmbeddedResource Include="Resources\RC_coords.txt" />
<EmbeddedResource Include="Resources\LG_remote.jpg" />
<EmbeddedResource Include="Resources\pngbarn.ico" />
<EmbeddedResource Include="Resources\SamsungRemotes.json" />
<Content Include="LgPresets.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="pngbarn.ico" />
</ItemGroup>
<ItemGroup>
<Folder Include="bin\x64\Debug\" />
</ItemGroup>
@@ -100,29 +97,30 @@
</COMReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.0" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2088.41" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="7.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.1.2" />
<PackageReference Include="NStandard" Version="0.24.0" />
<PackageReference Include="NWin32" Version="1.2.6" />
<PackageReference Include="System.Management" Version="7.0.0" />
<PackageReference Include="TaskScheduler" Version="2.10.1" />
<PackageReference Include="Vanara.PInvoke.IpHlpApi" Version="3.4.13" />
<PackageReference Include="RawInput.Sharp" Version="0.1.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.7" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3912.50" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="NLog" Version="6.1.2" />
<PackageReference Include="NStandard" Version="0.110.7" />
<PackageReference Include="NWin32" Version="1.2.8" />
<PackageReference Include="System.Management" Version="10.0.7" />
<PackageReference Include="TaskScheduler" Version="2.12.2" />
</ItemGroup>
<ItemGroup>
<Page Include="XForms\ColorProfileWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="XForms\BrowserWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="XForms\LogWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ColorControl.UI\ColorControl.UI.csproj" />
<ProjectReference Include="..\MHC2Gen\MHC2Gen.csproj" />
<ProjectReference Include="..\NLogViewer\NLogViewer.csproj" />
<ProjectReference Include="..\novideo_srgb\novideo_srgb.csproj" />
@@ -131,6 +129,10 @@
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Services\Samsung\SamsungPanel.resx">
<SubType>Designer</SubType>
</EmbeddedResource>
+25 -17
View File
@@ -1,15 +1,15 @@
using ColorControl.Forms;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows.Forms;
using ColorControl.Services.AMD;
using ColorControl.Services.LG;
using ColorControl.Services.NVIDIA;
using ColorControl.Services.Samsung;
using ColorControl.Shared.Common;
using ColorControl.Shared.Contracts;
using ColorControl.Svc;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace ColorControl
{
@@ -19,24 +19,22 @@ namespace ColorControl
{
if (startUpParams.StartElevated)
{
StartElevated();
await StartElevated();
return true;
}
var useConsole = startUpParams.NoGui || existingProcess != null;
var useConsole = startUpParams.NoGui || existingProcess?.SessionId > 0;
if (!useConsole)
{
return false;
}
startUpParams.NoGui = true;
var result = false;
if (startUpParams.ExecuteHelp)
{
Utils.OpenConsole();
PrepareConsole(startUpParams);
Console.WriteLine();
Console.WriteLine($@"ColorControl CLI {Application.ProductVersion}
@@ -56,7 +54,7 @@ Options:
if (startUpParams.ExecuteLgPreset)
{
Utils.OpenConsole();
PrepareConsole(startUpParams);
Console.WriteLine($"Executing LG-preset '{startUpParams.LgPresetName}'...");
await LgService.ExecutePresetAsync(startUpParams.LgPresetName);
@@ -68,7 +66,7 @@ Options:
if (startUpParams.ExecuteSamsungPreset)
{
Utils.OpenConsole();
PrepareConsole(startUpParams);
Console.WriteLine($"Executing Samsung-preset '{startUpParams.SamsungPresetName}'...");
await SamsungService.ExecutePresetAsync(startUpParams.SamsungPresetName);
@@ -80,7 +78,7 @@ Options:
if (startUpParams.ExecuteNvidiaPreset)
{
Utils.OpenConsole();
PrepareConsole(startUpParams);
Console.WriteLine($"Executing NVIDIA-preset '{startUpParams.NvidiaPresetIdOrName}'...");
await NvService.ExecutePresetAsync(startUpParams.NvidiaPresetIdOrName);
@@ -92,7 +90,7 @@ Options:
if (startUpParams.ExecuteAmdPreset)
{
Utils.OpenConsole();
PrepareConsole(startUpParams);
Console.WriteLine($"Executing AMD-preset '{startUpParams.AmdPresetIdOrName}'...");
await AmdService.ExecutePresetAsync(startUpParams.AmdPresetIdOrName);
@@ -105,13 +103,23 @@ Options:
return result;
}
private static void StartElevated()
private static void PrepareConsole(StartUpParams startUpParams)
{
Utils.OpenConsole();
startUpParams.NoGui = true;
}
private static async Task StartElevated()
{
try
{
var elevatedForm = Program.ServiceProvider.GetRequiredService<ElevatedForm>();
var backgroundService = Program.ServiceProvider.GetRequiredService<ColorControlBackgroundService>();
Application.Run(elevatedForm);
backgroundService.PipeName = PipeUtils.ElevatedPipe;
await backgroundService.StartAndStopWithMutex();
Application.Run();
}
catch (Exception ex)
{
-46
View File
@@ -1,46 +0,0 @@
namespace ColorControl.Forms
{
partial class ElevatedForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.SuspendLayout();
//
// ElevatedForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(251, 124);
this.Name = "ElevatedForm";
this.Text = "ElevatedForm";
this.ResumeLayout(false);
}
#endregion
}
}
-53
View File
@@ -1,53 +0,0 @@
using ColorControl.Svc;
using System;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace ColorControl.Forms;
public partial class ElevatedForm : Form
{
private ColorControlBackgroundService _backgroundService;
public ElevatedForm(ColorControlBackgroundService backgroundService)
{
InitializeComponent();
FormBorderStyle = FormBorderStyle.None;
ShowInTaskbar = false;
Load += ElevatedForm_Load;
Shown += ElevatedForm_Shown;
Task.Run(CheckMutexAsync);
_backgroundService = backgroundService;
_backgroundService.PipeName = "elevatedpipe";
Task.Run(async () => await _backgroundService.StartAsync(CancellationToken.None));
}
private void ElevatedForm_Shown(object sender, EventArgs e)
{
Hide();
}
protected void ElevatedForm_Load(object sender, EventArgs e)
{
Size = new Size(0, 0);
}
private void CheckMutexAsync()
{
var mutex = new Mutex(false, Shared.Common.GlobalContext.CurrentContext.MutexId, out var created);
try
{
mutex.WaitOne();
}
catch (AbandonedMutexException)
{
}
BeginInvoke(Application.Exit);
}
}
-60
View File
@@ -1,60 +0,0 @@
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+537
View File
@@ -0,0 +1,537 @@
using System.DirectoryServices.ActiveDirectory;
using System.Drawing;
using System;
namespace ColorControl.Forms
{
partial class OptionsPanel
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
grpOptionsModules = new System.Windows.Forms.GroupBox();
label1 = new System.Windows.Forms.Label();
chkModules = new System.Windows.Forms.CheckedListBox();
grpMiscellaneousOptions = new System.Windows.Forms.GroupBox();
btnOptionsColorProfiles = new System.Windows.Forms.Button();
btnSetShortcutScreenSaver = new System.Windows.Forms.Button();
label11 = new System.Windows.Forms.Label();
edtBlankScreenSaverShortcut = new System.Windows.Forms.TextBox();
lblFixChromeFontsDescription = new System.Windows.Forms.Label();
chkFixChromeFonts = new System.Windows.Forms.CheckBox();
grpGeneralOptions = new System.Windows.Forms.GroupBox();
btnOptionsLog = new System.Windows.Forms.Button();
btnOptionsAdvanced = new System.Windows.Forms.Button();
chkAutoInstallUpdates = new System.Windows.Forms.CheckBox();
btnStartStopService = new System.Windows.Forms.Button();
btnElevationInfo = new System.Windows.Forms.Button();
rbElevationService = new System.Windows.Forms.RadioButton();
rbElevationProcess = new System.Windows.Forms.RadioButton();
rbElevationAdmin = new System.Windows.Forms.RadioButton();
label8 = new System.Windows.Forms.Label();
rbElevationNone = new System.Windows.Forms.RadioButton();
chkCheckForUpdates = new System.Windows.Forms.CheckBox();
chkStartAfterLogin = new System.Windows.Forms.CheckBox();
chkOptionsUseDarkMode = new System.Windows.Forms.CheckBox();
chkGdiScaling = new System.Windows.Forms.CheckBox();
chkMinimizeToSystemTray = new System.Windows.Forms.CheckBox();
chkMinimizeOnClose = new System.Windows.Forms.CheckBox();
chkStartMinimized = new System.Windows.Forms.CheckBox();
mnuColorProfiles = new System.Windows.Forms.ContextMenuStrip(components);
miCreateHDRColorProfile = new System.Windows.Forms.ToolStripMenuItem();
miCreateSDRColorProfile = new System.Windows.Forms.ToolStripMenuItem();
lblUiType = new System.Windows.Forms.Label();
cbxUiType = new System.Windows.Forms.ComboBox();
grpUi = new System.Windows.Forms.GroupBox();
grpOptionsModules.SuspendLayout();
grpMiscellaneousOptions.SuspendLayout();
grpGeneralOptions.SuspendLayout();
mnuColorProfiles.SuspendLayout();
grpUi.SuspendLayout();
SuspendLayout();
//
// grpOptionsModules
//
grpOptionsModules.Controls.Add(label1);
grpOptionsModules.Controls.Add(chkModules);
grpOptionsModules.Location = new Point(14, 344);
grpOptionsModules.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
grpOptionsModules.Name = "grpOptionsModules";
grpOptionsModules.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
grpOptionsModules.Size = new Size(658, 131);
grpOptionsModules.TabIndex = 11;
grpOptionsModules.TabStop = false;
grpOptionsModules.Text = "Modules";
//
// label1
//
label1.AutoSize = true;
label1.Location = new Point(7, 19);
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
label1.Name = "label1";
label1.Size = new Size(511, 15);
label1.TabIndex = 7;
label1.Text = "Here you can disable modules you do not want. A restart is required for a change to have effect.";
//
// chkModules
//
chkModules.ColumnWidth = 200;
chkModules.FormattingEnabled = true;
chkModules.Location = new Point(7, 40);
chkModules.MultiColumn = true;
chkModules.Name = "chkModules";
chkModules.Size = new Size(507, 76);
chkModules.TabIndex = 0;
chkModules.ItemCheck += chkModules_ItemCheck;
//
// grpMiscellaneousOptions
//
grpMiscellaneousOptions.Controls.Add(btnOptionsColorProfiles);
grpMiscellaneousOptions.Controls.Add(btnSetShortcutScreenSaver);
grpMiscellaneousOptions.Controls.Add(label11);
grpMiscellaneousOptions.Controls.Add(edtBlankScreenSaverShortcut);
grpMiscellaneousOptions.Controls.Add(lblFixChromeFontsDescription);
grpMiscellaneousOptions.Controls.Add(chkFixChromeFonts);
grpMiscellaneousOptions.Location = new Point(14, 481);
grpMiscellaneousOptions.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
grpMiscellaneousOptions.Name = "grpMiscellaneousOptions";
grpMiscellaneousOptions.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
grpMiscellaneousOptions.Size = new Size(658, 131);
grpMiscellaneousOptions.TabIndex = 5;
grpMiscellaneousOptions.TabStop = false;
grpMiscellaneousOptions.Text = "Miscellaneous";
//
// btnOptionsColorProfiles
//
btnOptionsColorProfiles.Location = new Point(538, 17);
btnOptionsColorProfiles.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
btnOptionsColorProfiles.Name = "btnOptionsColorProfiles";
btnOptionsColorProfiles.Size = new Size(112, 27);
btnOptionsColorProfiles.TabIndex = 12;
btnOptionsColorProfiles.Text = "Color Profiles...";
btnOptionsColorProfiles.UseVisualStyleBackColor = true;
btnOptionsColorProfiles.Click += btnOptionsColorProfiles_Click;
//
// btnSetShortcutScreenSaver
//
btnSetShortcutScreenSaver.Location = new Point(181, 98);
btnSetShortcutScreenSaver.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
btnSetShortcutScreenSaver.Name = "btnSetShortcutScreenSaver";
btnSetShortcutScreenSaver.Size = new Size(40, 27);
btnSetShortcutScreenSaver.TabIndex = 10;
btnSetShortcutScreenSaver.Text = "Set";
btnSetShortcutScreenSaver.UseVisualStyleBackColor = true;
btnSetShortcutScreenSaver.Click += btnSetShortcutScreenSaver_Click;
//
// label11
//
label11.AutoSize = true;
label11.Location = new Point(7, 80);
label11.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
label11.Name = "label11";
label11.Size = new Size(238, 15);
label11.TabIndex = 9;
label11.Text = "Set a shortcut to start the blank screensaver:";
//
// edtBlankScreenSaverShortcut
//
edtBlankScreenSaverShortcut.Location = new Point(7, 101);
edtBlankScreenSaverShortcut.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
edtBlankScreenSaverShortcut.Name = "edtBlankScreenSaverShortcut";
edtBlankScreenSaverShortcut.ReadOnly = true;
edtBlankScreenSaverShortcut.Size = new Size(165, 23);
edtBlankScreenSaverShortcut.TabIndex = 7;
edtBlankScreenSaverShortcut.KeyDown += edtShortcut_KeyDown;
edtBlankScreenSaverShortcut.KeyUp += edtShortcut_KeyUp;
//
// lblFixChromeFontsDescription
//
lblFixChromeFontsDescription.AutoSize = true;
lblFixChromeFontsDescription.Location = new Point(4, 45);
lblFixChromeFontsDescription.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
lblFixChromeFontsDescription.Name = "lblFixChromeFontsDescription";
lblFixChromeFontsDescription.Size = new Size(427, 15);
lblFixChromeFontsDescription.TabIndex = 6;
lblFixChromeFontsDescription.Text = "This will add the parameter --disable-lcd-text to Chrome and requires elevation.";
//
// chkFixChromeFonts
//
chkFixChromeFonts.AutoSize = true;
chkFixChromeFonts.Location = new Point(7, 22);
chkFixChromeFonts.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkFixChromeFonts.Name = "chkFixChromeFonts";
chkFixChromeFonts.Size = new Size(378, 19);
chkFixChromeFonts.TabIndex = 4;
chkFixChromeFonts.Text = "ClearType: fix bad fonts in Chrome (turn on grayscale anti-aliasing)";
chkFixChromeFonts.UseVisualStyleBackColor = true;
chkFixChromeFonts.CheckedChanged += chkFixChromeFonts_CheckedChanged;
//
// grpGeneralOptions
//
grpGeneralOptions.Controls.Add(btnOptionsLog);
grpGeneralOptions.Controls.Add(btnOptionsAdvanced);
grpGeneralOptions.Controls.Add(chkAutoInstallUpdates);
grpGeneralOptions.Controls.Add(btnStartStopService);
grpGeneralOptions.Controls.Add(btnElevationInfo);
grpGeneralOptions.Controls.Add(rbElevationService);
grpGeneralOptions.Controls.Add(rbElevationProcess);
grpGeneralOptions.Controls.Add(rbElevationAdmin);
grpGeneralOptions.Controls.Add(label8);
grpGeneralOptions.Controls.Add(rbElevationNone);
grpGeneralOptions.Controls.Add(chkCheckForUpdates);
grpGeneralOptions.Controls.Add(chkStartAfterLogin);
grpGeneralOptions.Location = new Point(7, 6);
grpGeneralOptions.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
grpGeneralOptions.Name = "grpGeneralOptions";
grpGeneralOptions.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
grpGeneralOptions.Size = new Size(662, 202);
grpGeneralOptions.TabIndex = 2;
grpGeneralOptions.TabStop = false;
grpGeneralOptions.Text = "General";
//
// btnOptionsLog
//
btnOptionsLog.Location = new Point(405, 47);
btnOptionsLog.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
btnOptionsLog.Name = "btnOptionsLog";
btnOptionsLog.Size = new Size(88, 27);
btnOptionsLog.TabIndex = 68;
btnOptionsLog.Text = "Logging...";
btnOptionsLog.UseVisualStyleBackColor = true;
btnOptionsLog.Click += btnOptionsLog_Click;
//
// btnOptionsAdvanced
//
btnOptionsAdvanced.Location = new Point(405, 17);
btnOptionsAdvanced.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
btnOptionsAdvanced.Name = "btnOptionsAdvanced";
btnOptionsAdvanced.Size = new Size(88, 27);
btnOptionsAdvanced.TabIndex = 11;
btnOptionsAdvanced.Text = "Advanced...";
btnOptionsAdvanced.UseVisualStyleBackColor = true;
btnOptionsAdvanced.Click += btnOptionsAdvanced_Click;
//
// chkAutoInstallUpdates
//
chkAutoInstallUpdates.AutoSize = true;
chkAutoInstallUpdates.Location = new Point(7, 72);
chkAutoInstallUpdates.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkAutoInstallUpdates.Name = "chkAutoInstallUpdates";
chkAutoInstallUpdates.Size = new Size(179, 19);
chkAutoInstallUpdates.TabIndex = 66;
chkAutoInstallUpdates.Text = "Automatically install updates";
chkAutoInstallUpdates.UseVisualStyleBackColor = true;
chkAutoInstallUpdates.CheckedChanged += chkAutoInstallUpdates_CheckedChanged;
//
// btnStartStopService
//
btnStartStopService.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
btnStartStopService.Location = new Point(405, 150);
btnStartStopService.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
btnStartStopService.Name = "btnStartStopService";
btnStartStopService.Size = new Size(40, 27);
btnStartStopService.TabIndex = 65;
btnStartStopService.Text = "Start";
btnStartStopService.UseVisualStyleBackColor = true;
btnStartStopService.Click += btnStartStopService_Click;
//
// btnElevationInfo
//
btnElevationInfo.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
btnElevationInfo.Location = new Point(120, 88);
btnElevationInfo.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
btnElevationInfo.Name = "btnElevationInfo";
btnElevationInfo.Size = new Size(22, 27);
btnElevationInfo.TabIndex = 64;
btnElevationInfo.Text = "?";
btnElevationInfo.UseVisualStyleBackColor = true;
btnElevationInfo.Click += btnElevationInfo_Click;
//
// rbElevationService
//
rbElevationService.AutoSize = true;
rbElevationService.Location = new Point(7, 154);
rbElevationService.Name = "rbElevationService";
rbElevationService.Size = new Size(136, 19);
rbElevationService.TabIndex = 63;
rbElevationService.TabStop = true;
rbElevationService.Tag = "2";
rbElevationService.Text = "Use Windows Service";
rbElevationService.UseVisualStyleBackColor = true;
rbElevationService.CheckedChanged += rbElevationNone_CheckedChanged;
//
// rbElevationProcess
//
rbElevationProcess.AutoSize = true;
rbElevationProcess.Location = new Point(7, 175);
rbElevationProcess.Name = "rbElevationProcess";
rbElevationProcess.Size = new Size(189, 19);
rbElevationProcess.TabIndex = 62;
rbElevationProcess.TabStop = true;
rbElevationProcess.Tag = "3";
rbElevationProcess.Text = "Use dedicated elevated process";
rbElevationProcess.UseVisualStyleBackColor = true;
rbElevationProcess.CheckedChanged += rbElevationNone_CheckedChanged;
//
// rbElevationAdmin
//
rbElevationAdmin.AutoSize = true;
rbElevationAdmin.Location = new Point(7, 133);
rbElevationAdmin.Name = "rbElevationAdmin";
rbElevationAdmin.Size = new Size(97, 19);
rbElevationAdmin.TabIndex = 61;
rbElevationAdmin.TabStop = true;
rbElevationAdmin.Tag = "1";
rbElevationAdmin.Text = "Run as admin";
rbElevationAdmin.UseVisualStyleBackColor = true;
rbElevationAdmin.CheckedChanged += rbElevationNone_CheckedChanged;
//
// label8
//
label8.AutoSize = true;
label8.Location = new Point(7, 94);
label8.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
label8.Name = "label8";
label8.Size = new Size(105, 15);
label8.TabIndex = 60;
label8.Text = "Elevation-method:";
//
// rbElevationNone
//
rbElevationNone.AutoSize = true;
rbElevationNone.Location = new Point(7, 112);
rbElevationNone.Name = "rbElevationNone";
rbElevationNone.Size = new Size(54, 19);
rbElevationNone.TabIndex = 10;
rbElevationNone.TabStop = true;
rbElevationNone.Tag = "0";
rbElevationNone.Text = "None";
rbElevationNone.UseVisualStyleBackColor = true;
rbElevationNone.CheckedChanged += rbElevationNone_CheckedChanged;
//
// chkCheckForUpdates
//
chkCheckForUpdates.AutoSize = true;
chkCheckForUpdates.Location = new Point(7, 47);
chkCheckForUpdates.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkCheckForUpdates.Name = "chkCheckForUpdates";
chkCheckForUpdates.Size = new Size(197, 19);
chkCheckForUpdates.TabIndex = 6;
chkCheckForUpdates.Text = "Automatically check for updates";
chkCheckForUpdates.UseVisualStyleBackColor = true;
chkCheckForUpdates.CheckedChanged += chkCheckForUpdates_CheckedChanged;
//
// chkStartAfterLogin
//
chkStartAfterLogin.AutoSize = true;
chkStartAfterLogin.Location = new Point(7, 22);
chkStartAfterLogin.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkStartAfterLogin.Name = "chkStartAfterLogin";
chkStartAfterLogin.Size = new Size(183, 19);
chkStartAfterLogin.TabIndex = 2;
chkStartAfterLogin.Text = "Automatically start after login";
chkStartAfterLogin.UseVisualStyleBackColor = true;
chkStartAfterLogin.CheckedChanged += chkStartAfterLogin_CheckedChanged;
//
// chkOptionsUseDarkMode
//
chkOptionsUseDarkMode.AutoSize = true;
chkOptionsUseDarkMode.Location = new Point(329, 22);
chkOptionsUseDarkMode.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkOptionsUseDarkMode.Name = "chkOptionsUseDarkMode";
chkOptionsUseDarkMode.Size = new Size(224, 19);
chkOptionsUseDarkMode.TabIndex = 67;
chkOptionsUseDarkMode.Text = "Dark Mode (WinForms: experimental)";
chkOptionsUseDarkMode.UseVisualStyleBackColor = true;
chkOptionsUseDarkMode.CheckedChanged += chkOptionsUseDarkMode_CheckedChanged;
//
// chkGdiScaling
//
chkGdiScaling.AutoSize = true;
chkGdiScaling.Location = new Point(329, 47);
chkGdiScaling.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkGdiScaling.Name = "chkGdiScaling";
chkGdiScaling.Size = new Size(257, 19);
chkGdiScaling.TabIndex = 7;
chkGdiScaling.Text = "WinForms: Use GDI Scaling (requires restart)";
chkGdiScaling.UseVisualStyleBackColor = true;
chkGdiScaling.CheckedChanged += chkGdiScaling_CheckedChanged;
//
// chkMinimizeToSystemTray
//
chkMinimizeToSystemTray.AutoSize = true;
chkMinimizeToSystemTray.Location = new Point(7, 97);
chkMinimizeToSystemTray.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkMinimizeToSystemTray.Name = "chkMinimizeToSystemTray";
chkMinimizeToSystemTray.Size = new Size(152, 19);
chkMinimizeToSystemTray.TabIndex = 5;
chkMinimizeToSystemTray.Text = "Minimize to system tray";
chkMinimizeToSystemTray.UseVisualStyleBackColor = true;
chkMinimizeToSystemTray.CheckedChanged += chkMinimizeToSystemTray_CheckedChanged;
//
// chkMinimizeOnClose
//
chkMinimizeOnClose.AutoSize = true;
chkMinimizeOnClose.Location = new Point(7, 72);
chkMinimizeOnClose.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkMinimizeOnClose.Name = "chkMinimizeOnClose";
chkMinimizeOnClose.Size = new Size(122, 19);
chkMinimizeOnClose.TabIndex = 4;
chkMinimizeOnClose.Text = "Minimize on close";
chkMinimizeOnClose.UseVisualStyleBackColor = true;
chkMinimizeOnClose.CheckedChanged += chkMinimizeOnClose_CheckedChanged;
//
// chkStartMinimized
//
chkStartMinimized.AutoSize = true;
chkStartMinimized.Location = new Point(7, 47);
chkStartMinimized.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkStartMinimized.Name = "chkStartMinimized";
chkStartMinimized.Size = new Size(109, 19);
chkStartMinimized.TabIndex = 3;
chkStartMinimized.Text = "Start minimized";
chkStartMinimized.UseVisualStyleBackColor = true;
chkStartMinimized.CheckedChanged += chkStartMinimized_CheckedChanged;
//
// mnuColorProfiles
//
mnuColorProfiles.ImageScalingSize = new Size(20, 20);
mnuColorProfiles.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { miCreateHDRColorProfile, miCreateSDRColorProfile });
mnuColorProfiles.Name = "mnuLgButtons";
mnuColorProfiles.Size = new Size(214, 48);
//
// miCreateHDRColorProfile
//
miCreateHDRColorProfile.Name = "miCreateHDRColorProfile";
miCreateHDRColorProfile.Size = new Size(213, 22);
miCreateHDRColorProfile.Text = "Create HDR Color Profile...";
miCreateHDRColorProfile.Click += miCreateHDRColorProfile_Click;
//
// miCreateSDRColorProfile
//
miCreateSDRColorProfile.Name = "miCreateSDRColorProfile";
miCreateSDRColorProfile.Size = new Size(213, 22);
miCreateSDRColorProfile.Text = "Create SDR Color Profile...";
miCreateSDRColorProfile.Click += miCreateSDRColorProfile_Click;
//
// lblUiType
//
lblUiType.AutoSize = true;
lblUiType.Location = new Point(6, 21);
lblUiType.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
lblUiType.Name = "lblUiType";
lblUiType.Size = new Size(62, 15);
lblUiType.TabIndex = 70;
lblUiType.Text = "Default UI:";
//
// cbxUiType
//
cbxUiType.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
cbxUiType.FormattingEnabled = true;
cbxUiType.Items.AddRange(new object[] { "WinForms", "Web (default browser)", "Web (embedded browser)" });
cbxUiType.Location = new Point(105, 18);
cbxUiType.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
cbxUiType.Name = "cbxUiType";
cbxUiType.Size = new Size(208, 23);
cbxUiType.TabIndex = 69;
cbxUiType.SelectedIndexChanged += cbxUiType_SelectedIndexChanged;
//
// grpUi
//
grpUi.Controls.Add(lblUiType);
grpUi.Controls.Add(chkStartMinimized);
grpUi.Controls.Add(chkOptionsUseDarkMode);
grpUi.Controls.Add(cbxUiType);
grpUi.Controls.Add(chkMinimizeOnClose);
grpUi.Controls.Add(chkMinimizeToSystemTray);
grpUi.Controls.Add(chkGdiScaling);
grpUi.Location = new Point(7, 214);
grpUi.Name = "grpUi";
grpUi.Size = new Size(662, 124);
grpUi.TabIndex = 8;
grpUi.TabStop = false;
grpUi.Text = "UI-settings";
//
// OptionsPanel
//
Controls.Add(grpUi);
Controls.Add(grpOptionsModules);
Controls.Add(grpMiscellaneousOptions);
Controls.Add(grpGeneralOptions);
Name = "OptionsPanel";
Size = new Size(732, 615);
grpOptionsModules.ResumeLayout(false);
grpOptionsModules.PerformLayout();
grpMiscellaneousOptions.ResumeLayout(false);
grpMiscellaneousOptions.PerformLayout();
grpGeneralOptions.ResumeLayout(false);
grpGeneralOptions.PerformLayout();
mnuColorProfiles.ResumeLayout(false);
grpUi.ResumeLayout(false);
grpUi.PerformLayout();
ResumeLayout(false);
}
private System.Windows.Forms.GroupBox grpGeneralOptions;
private System.Windows.Forms.CheckBox chkStartMinimized;
private System.Windows.Forms.CheckBox chkStartAfterLogin;
private System.Windows.Forms.GroupBox grpMiscellaneousOptions;
private System.Windows.Forms.CheckBox chkFixChromeFonts;
private System.Windows.Forms.Label lblFixChromeFontsDescription;
private System.Windows.Forms.Button btnSetShortcutScreenSaver;
private System.Windows.Forms.Label label11;
private System.Windows.Forms.TextBox edtBlankScreenSaverShortcut;
private System.Windows.Forms.CheckBox chkMinimizeOnClose;
private System.Windows.Forms.CheckBox chkMinimizeToSystemTray;
private System.Windows.Forms.CheckBox chkCheckForUpdates;
private System.Windows.Forms.CheckBox chkGdiScaling;
private System.Windows.Forms.RadioButton rbElevationService;
private System.Windows.Forms.RadioButton rbElevationProcess;
private System.Windows.Forms.RadioButton rbElevationAdmin;
private System.Windows.Forms.Label label8;
private System.Windows.Forms.RadioButton rbElevationNone;
private System.Windows.Forms.Button btnElevationInfo;
private System.Windows.Forms.Button btnStartStopService;
private System.Windows.Forms.CheckBox chkAutoInstallUpdates;
private System.Windows.Forms.GroupBox grpOptionsModules;
private System.Windows.Forms.CheckedListBox chkModules;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.CheckBox chkOptionsUseDarkMode;
private System.Windows.Forms.Button btnOptionsAdvanced;
private System.Windows.Forms.Button btnOptionsLog;
private System.Windows.Forms.Button btnOptionsColorProfiles;
private System.Windows.Forms.ContextMenuStrip mnuColorProfiles;
private System.Windows.Forms.ToolStripMenuItem miCreateHDRColorProfile;
private System.Windows.Forms.ToolStripMenuItem miCreateSDRColorProfile;
#endregion
private System.Windows.Forms.Label lblUiType;
private System.Windows.Forms.ComboBox cbxUiType;
private System.Windows.Forms.GroupBox grpUi;
}
}
+391
View File
@@ -0,0 +1,391 @@
using ColorControl.Services.Common;
using ColorControl.Shared.Common;
using ColorControl.Shared.Contracts;
using ColorControl.Shared.EventDispatcher;
using ColorControl.Shared.Forms;
using ColorControl.Shared.Services;
using ColorControl.XForms;
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace ColorControl.Forms;
public partial class OptionsPanel : UserControl
{
private readonly WinApiService _winApiService;
private readonly WinApiAdminService _winApiAdminService;
private readonly ElevationService _elevationService;
private readonly KeyboardShortcutDispatcher _keyboardShortcutDispatcher;
private readonly OptionsService _optionsService;
private bool _initialized;
private Config _config;
public OptionsPanel(WinApiService winApiService, WinApiAdminService winApiAdminService, ElevationService elevationService, KeyboardShortcutDispatcher keyboardShortcutDispatcher, OptionsService optionsService)
{
_winApiService = winApiService;
_winApiAdminService = winApiAdminService;
_elevationService = elevationService;
_keyboardShortcutDispatcher = keyboardShortcutDispatcher;
_optionsService = optionsService;
_config = _optionsService.GetConfig();
InitializeComponent();
if (DarkModeUtils.UseDarkMode)
{
for (var i = 0; i < Controls.Count; i++)
{
var control = Controls[i];
DarkModeUtils.SetControlTheme(control);
}
}
Init();
UpdateServiceInfo();
}
private void Init()
{
if (chkModules.Items.Count == 0)
{
foreach (var module in _config.Modules)
{
chkModules.Items.Add(module.DisplayName, module.IsActive);
}
}
chkStartMinimized.Checked = _config.StartMinimized;
chkMinimizeOnClose.Checked = _config.MinimizeOnClose;
chkMinimizeToSystemTray.Checked = _config.MinimizeToTray;
chkCheckForUpdates.Checked = _config.CheckForUpdates;
chkAutoInstallUpdates.Checked = _config.AutoInstallUpdates;
chkAutoInstallUpdates.Enabled = _config.CheckForUpdates && _config.ElevationMethod == ElevationMethod.UseService;
edtBlankScreenSaverShortcut.Text = _config.ScreenSaverShortcut;
chkGdiScaling.Checked = _config.UseGdiScaling;
chkOptionsUseDarkMode.Checked = _config.UseDarkMode;
chkStartAfterLogin.Checked = _winApiService.TaskExists(Program.TS_TASKNAME);
chkFixChromeFonts.Enabled = _winApiService.IsChromeInstalled();
if (chkFixChromeFonts.Enabled)
{
chkFixChromeFonts.Checked = _winApiService.IsChromeFixInstalled();
}
_initialized = false;
try
{
var _ = _config.ElevationMethod switch
{
ElevationMethod.None => rbElevationNone.Checked = true,
ElevationMethod.RunAsAdmin => rbElevationAdmin.Checked = true,
ElevationMethod.UseService => rbElevationService.Checked = true,
ElevationMethod.UseElevatedProcess => rbElevationProcess.Checked = true,
_ => false
};
cbxUiType.SelectedIndex = (int)_config.UiType;
}
finally
{
_initialized = true;
}
}
private void UpdateServiceInfo()
{
var text = "Use Windows Service";
btnStartStopService.Enabled = true;
if (_winApiService.IsServiceRunning())
{
text += " (running)";
btnStartStopService.Text = "Stop";
}
else if (_winApiService.IsServiceInstalled())
{
text += " (installed, not running)";
btnStartStopService.Text = "Start";
}
else
{
text += " (not installed)";
btnStartStopService.Text = "Start";
btnStartStopService.Enabled = false;
}
rbElevationService.Text = text;
}
private void edtShortcut_KeyDown(object sender, KeyEventArgs e)
{
((TextBox)sender).Text = KeyboardShortcutDispatcher.FormatKeyboardShortcut(e);
}
private void edtShortcut_KeyUp(object sender, KeyEventArgs e)
{
KeyboardShortcutDispatcher.HandleKeyboardShortcutUp(e);
}
private void btnSetShortcutScreenSaver_Click(object sender, EventArgs e)
{
var shortcut = edtBlankScreenSaverShortcut.Text.Trim();
if (!KeyboardShortcutDispatcher.ValidateShortcut(shortcut))
{
return;
}
_config.ScreenSaverShortcut = shortcut;
_keyboardShortcutDispatcher.RegisterShortcut(MainWorker.SHORTCUTID_SCREENSAVER, shortcut);
}
private void chkFixChromeFonts_CheckedChanged(object sender, EventArgs e)
{
if (_initialized)
{
_config.FixChromeFonts = chkFixChromeFonts.Checked;
var folder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
_winApiAdminService.InstallChromeFix(chkFixChromeFonts.Checked, folder);
}
}
private void chkAutoInstallUpdates_CheckedChanged(object sender, EventArgs e)
{
if (_initialized)
{
_config.AutoInstallUpdates = chkAutoInstallUpdates.Checked;
}
}
private void chkMinimizeToSystemTray_CheckedChanged(object sender, EventArgs e)
{
if (_initialized)
{
_optionsService.SetMinimizeToTray(chkMinimizeToSystemTray.Checked);
}
}
private void chkCheckForUpdates_CheckedChanged(object sender, EventArgs e)
{
if (_initialized)
{
_config.CheckForUpdates = chkCheckForUpdates.Checked;
chkAutoInstallUpdates.Enabled = _config.CheckForUpdates && _config.ElevationMethod == ElevationMethod.UseService;
}
}
private void chkGdiScaling_CheckedChanged(object sender, EventArgs e)
{
_config.UseGdiScaling = chkGdiScaling.Checked;
}
private void chkStartAfterLogin_CheckedChanged(object sender, EventArgs e)
{
if (!_initialized)
{
return;
}
var enabled = chkStartAfterLogin.Checked;
_optionsService.SetStartAfterLogin(enabled);
rbElevationAdmin.Enabled = enabled;
if (!enabled && rbElevationAdmin.Checked)
{
rbElevationNone.Checked = true;
}
}
private void btnElevationInfo_Click(object sender, EventArgs e)
{
MessageForms.InfoOk(Utils.ELEVATION_MSG + @$"
NOTE: if ColorControl itself already runs as administrator this is indicated in the title: ColorControl {Application.ProductVersion} (administrator).
Currently ColorControl is {(_winApiService.IsAdministrator() ? "" : "not ")}running as administrator.
");
}
private void rbElevationNone_CheckedChanged(object sender, EventArgs e)
{
if (!_initialized)
{
return;
}
if (sender is not RadioButton button || !button.Checked)
{
return;
}
_elevationService.SetElevationMethod((ElevationMethod)Utils.ParseInt((string)button.Tag), chkStartAfterLogin.Checked);
UpdateServiceInfo();
chkAutoInstallUpdates.Enabled = _config.CheckForUpdates && _config.ElevationMethod == ElevationMethod.UseService;
}
private async void btnStartStopService_Click(object sender, EventArgs e)
{
btnStartStopService.Enabled = false;
try
{
var start = btnStartStopService.Text == "Start";
if (start)
{
_winApiAdminService.StartService();
}
else
{
_winApiAdminService.StopService();
}
var wait = 1500;
while (wait > 0)
{
await Task.Delay(100);
if (start == _winApiService.IsServiceRunning())
{
break;
}
wait -= 100;
}
}
finally
{
btnStartStopService.Enabled = true;
}
UpdateServiceInfo();
}
private void chkMinimizeOnClose_CheckedChanged(object sender, EventArgs e)
{
_config.MinimizeOnClose = chkMinimizeOnClose.Checked;
}
private void chkOptionsUseDarkMode_CheckedChanged(object sender, EventArgs e)
{
if (!_initialized)
{
return;
}
_optionsService.SetDarkMode(chkOptionsUseDarkMode.Checked);
}
private void btnOptionsAdvanced_Click(object sender, EventArgs e)
{
var processPollingIntervalField = new FieldDefinition
{
FieldType = FieldType.Numeric,
Label = "Polling interval of process monitor (milliseconds).",
SubLabel = "Decreasing this delay may execute triggered presets sooner but can cause a higher CPU load",
MinValue = 50,
MaxValue = 5000,
Value = _config.ProcessMonitorPollingInterval
};
var useRawInputField = new FieldDefinition
{
FieldType = FieldType.CheckBox,
Label = "Use Raw Input for shortcuts (hot keys)",
SubLabel = "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",
Value = _config.UseRawInput
};
var setMinTmlAndMaxTmlField = new FieldDefinition
{
FieldType = FieldType.CheckBox,
Label = "Set MinTML and MaxTML when applying color profiles",
SubLabel = "When this is enabled MinTML and MaxTML will be automatically be set to respectively the minimum luminance and the maximum luminance of the color profile",
Value = _config.SetMinTmlAndMaxTml
};
var disableErrorPopupField = new FieldDefinition
{
FieldType = FieldType.CheckBox,
Label = "Disable error popup when applying presets",
SubLabel = "Check this to disable the error popup when there was an error applying a preset",
Value = _config.DisableErrorPopupWhenApplyingPreset
};
var values = MessageForms.ShowDialog("Advanced settings", new[] { processPollingIntervalField, useRawInputField, setMinTmlAndMaxTmlField, disableErrorPopupField });
if (values?.Any() != true)
{
return;
}
_config.ProcessMonitorPollingInterval = processPollingIntervalField.ValueAsInt;
_config.UseRawInput = useRawInputField.ValueAsBool;
_config.SetMinTmlAndMaxTml = setMinTmlAndMaxTmlField.ValueAsBool;
_config.DisableErrorPopupWhenApplyingPreset = disableErrorPopupField.ValueAsBool;
_keyboardShortcutDispatcher.SetUseRawInput(_config.UseRawInput);
}
private void btnOptionsLog_Click(object sender, EventArgs e)
{
LogWindow.CreateAndShow();
}
private void chkModules_ItemCheck(object sender, ItemCheckEventArgs e)
{
if (chkModules.Items.Count != _config.Modules.Count)
{
return;
}
var index = e.Index;
if (index < 0)
{
return;
}
var module = _config.Modules[index];
module.IsActive = e.NewValue == CheckState.Checked;
}
private void miCreateHDRColorProfile_Click(object sender, EventArgs e)
{
ColorProfileWindow.CreateAndShow();
}
private void btnOptionsColorProfiles_Click(object sender, EventArgs e)
{
mnuColorProfiles.ShowCustom(btnOptionsColorProfiles);
}
private void miCreateSDRColorProfile_Click(object sender, EventArgs e)
{
ColorProfileWindow.CreateAndShow(isHDR: false);
}
private void chkStartMinimized_CheckedChanged(object sender, EventArgs e)
{
if (_initialized)
{
_config.StartMinimized = chkStartMinimized.Checked;
}
}
private void cbxUiType_SelectedIndexChanged(object sender, EventArgs e)
{
if (!_initialized)
{
return;
}
_optionsService.SetUiType((UiType)cbxUiType.SelectedIndex);
}
}
+123
View File
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="mnuColorProfiles.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>
+1
View File
@@ -3,5 +3,6 @@
internal interface IModulePanel
{
public void UpdateInfo();
public void Init();
}
}
+302
View File
@@ -0,0 +1,302 @@
using ColorControl.Shared.Common;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Concurrent;
using System.Net.Security;
using System.Net.WebSockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace LgTv
{
public class SendMessageExceptionCws : Exception
{
public SendMessageExceptionCws(string message, Exception e) : base(message, e)
{
}
}
public delegate void IsConnectedDelegateCws(bool status);
public class LgTvApiCoreCws : IAsyncDisposable
{
public bool ConnectionClosed { get; private set; }
private ClientWebSocket _clientWebSocket;
private int _commandCount;
private Uri _uri;
public event IsConnectedDelegateCws IsConnected;
private readonly ConcurrentDictionary<string, TaskCompletionSource<dynamic>> _tokens = new ConcurrentDictionary<string, TaskCompletionSource<dynamic>>();
private readonly ConcurrentDictionary<string, Func<dynamic, bool>> _callbacks = new ConcurrentDictionary<string, Func<dynamic, bool>>();
private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger();
public async Task<bool> Connect(Uri uri, bool ignoreReceiver = false)
{
_uri = uri;
try
{
ConnectionClosed = false;
_commandCount = 0;
CreateWebSocket();
try
{
await _clientWebSocket.ConnectAsync(uri, CancellationToken.None).WaitAsync(TimeSpan.FromMilliseconds(5000));
ConnectionClosed = false;
var _ = Receive(false);
IsConnected?.Invoke(true);
return true;
}
catch (TimeoutException te)
{
Logger.Error($"Timeout while connecting to {uri}: {te.Message}");
return false;
}
}
catch (Exception e)
{
Logger.Error($"Connect to {uri}: {e.Message}");
return false;
}
}
private void CreateWebSocket()
{
_clientWebSocket = new ClientWebSocket();
_clientWebSocket.Options.RemoteCertificateValidationCallback += CertificationValidationCallback;
}
private bool CertificationValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors policyErrors) => true;
public async Task SendMessageAsync(string message, bool reconnect = true)
{
try
{
if (_clientWebSocket?.State != WebSocketState.Open)
{
throw new InvalidOperationException("Socket not open");
}
var bytes = UTF8Encoding.UTF8.GetBytes(message);
var data = bytes.AsMemory();
await _clientWebSocket.SendAsync(data, WebSocketMessageType.Text, true, CancellationToken.None);
}
catch (WebSocketException)
{
if (reconnect && await Connect(_uri))
{
await SendMessageAsync(message, false);
}
}
catch (Exception e)
{
Logger.Error("SendMessageAsync: " + e.Message);
ConnectionClosed = true;
}
}
public async Task<dynamic> SendCommandAsync(string message)
{
var obj = JsonConvert.DeserializeObject<dynamic>(message);
return await SendCommandAsync((string)obj.id, message);
}
private async Task<dynamic> SendCommandAsync(string id, string message)
{
try
{
var taskSource = new TaskCompletionSource<dynamic>();
_tokens.TryAdd(id, taskSource);
await SendMessageAsync(message);
if (ConnectionClosed)
{
throw new Exception("Connection closed");
}
return await taskSource.Task;
}
catch (Exception e)
{
throw new SendMessageExceptionCws("Can't send message", e);
}
}
public async Task<dynamic> SendCommandAsync(RequestMessage message)
{
var rawMessage = new RawRequestMessage(message, ++_commandCount);
var serialized = JsonConvert.SerializeObject(rawMessage, new JsonSerializerSettings()
{
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
return await SendCommandAsync(rawMessage.Id, serialized);
}
public async Task<dynamic> SubscribeAsync(RequestMessage message, Func<dynamic, bool> callback)
{
var rawMessage = new RawRequestMessage(message, ++_commandCount);
var serialized = JsonConvert.SerializeObject(rawMessage, new JsonSerializerSettings()
{
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
_callbacks.TryAdd(rawMessage.Id, callback);
return await SendCommandAsync(rawMessage.Id, serialized);
}
private async Task Receive(bool singleResponse)
{
var dataPerPacket = 8 * 1024; //8192
var strBuilder = new StringBuilder();
var buffer = new ArraySegment<byte>(new byte[dataPerPacket]);
try
{
while (_clientWebSocket?.State == WebSocketState.Open)
{
var receiveResult = await _clientWebSocket.ReceiveAsync(buffer, CancellationToken.None);
strBuilder.Append(Encoding.UTF8.GetString(buffer.Array, buffer.Offset, receiveResult.Count));
//keep doing until end of message
while (!receiveResult.EndOfMessage)
{
receiveResult = await _clientWebSocket.ReceiveAsync(buffer, CancellationToken.None);
strBuilder.Append(Encoding.UTF8.GetString(buffer.Array, buffer.Offset, receiveResult.Count));
}
var message = strBuilder.ToString();
strBuilder.Clear();
HandleMessage(message);
if (singleResponse)
{
return;
}
}
}
catch (Exception)
{
ConnectionClosed = true;
Logger.Debug("Connection closed");
IsConnected?.Invoke(false);
}
}
public void HandleMessage(string message)
{
try
{
if (string.IsNullOrEmpty(message))
{
throw new InvalidOperationException("Empty response received. Assuming connection closed.");
}
var obj = JsonConvert.DeserializeObject<dynamic>(message);
var id = (string)obj.id;
var type = (string)obj.type;
TaskCompletionSource<dynamic> taskCompletion;
if (type == "registered")
{
if (_tokens.TryRemove(id, out taskCompletion))
{
var key = (string)JObject.Parse(message)["payload"]["client-key"];
taskCompletion.TrySetResult(new { clientKey = key });
}
}
else if (_tokens.TryGetValue(id, out taskCompletion))
{
if (id == "register_0")
{
//taskCompletion.TrySetResult(null);
return;
}
if (obj.type == "error")
{
taskCompletion.SetException(new Exception(obj.error?.ToString()));
}
//else if (args.Cancelled)
//{
// taskSource.SetCanceled();
//}
taskCompletion.TrySetResult(obj.payload);
if (_callbacks.TryGetValue(id, out Func<dynamic, bool> callback))
{
try
{
callback(obj.payload);
}
catch (Exception callbackException)
{
Logger.Error($"Connection_MessageReceived: the callback threw an exception: {callbackException.ToLogString()}");
}
}
}
}
catch (Exception ex)
{
Logger.Error($"HandleMessage: status: {_clientWebSocket.State}, exception: {ex.ToLogString()}");
SetExceptionOnAllTokens(ex);
ConnectionClosed = true;
}
}
public async ValueTask DisposeAsync()
{
if (_clientWebSocket != null)
{
if (_clientWebSocket.State == WebSocketState.Open)
{
try
{
await _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
}
catch { }
}
_clientWebSocket?.Dispose();
}
GC.SuppressFinalize(this);
}
private void SetExceptionOnAllTokens(Exception ex)
{
//Logger.Debug($"Tokens: {_tokens.Count}");
foreach (var taskCompletion in _tokens.Values)
{
try
{
if (taskCompletion.Task.Status != TaskStatus.RanToCompletion)
{
Logger.Debug("taskCompletion.Task.Status: " + taskCompletion.Task.Status);
taskCompletion.SetException(new Exception(ex.Message));
}
}
catch (Exception ex2)
{
Logger.Error("SetExceptionOnAllTokens: " + ex2.Message);
}
}
_tokens.Clear();
}
}
}
+1 -439
View File
@@ -31,45 +31,11 @@
components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
tcMain = new System.Windows.Forms.TabControl();
tabOptions = new System.Windows.Forms.TabPage();
grpOptionsModules = new System.Windows.Forms.GroupBox();
label1 = new System.Windows.Forms.Label();
chkModules = new System.Windows.Forms.CheckedListBox();
grpMiscellaneousOptions = new System.Windows.Forms.GroupBox();
btnOptionsColorProfiles = new System.Windows.Forms.Button();
btnSetShortcutScreenSaver = new System.Windows.Forms.Button();
label11 = new System.Windows.Forms.Label();
edtBlankScreenSaverShortcut = new System.Windows.Forms.TextBox();
lblFixChromeFontsDescription = new System.Windows.Forms.Label();
chkFixChromeFonts = new System.Windows.Forms.CheckBox();
grpGeneralOptions = new System.Windows.Forms.GroupBox();
btnOptionsLog = new System.Windows.Forms.Button();
btnOptionsAdvanced = new System.Windows.Forms.Button();
chkOptionsUseDarkMode = new System.Windows.Forms.CheckBox();
chkAutoInstallUpdates = new System.Windows.Forms.CheckBox();
btnStartStopService = new System.Windows.Forms.Button();
btnElevationInfo = new System.Windows.Forms.Button();
rbElevationService = new System.Windows.Forms.RadioButton();
rbElevationProcess = new System.Windows.Forms.RadioButton();
rbElevationAdmin = new System.Windows.Forms.RadioButton();
label8 = new System.Windows.Forms.Label();
rbElevationNone = new System.Windows.Forms.RadioButton();
chkGdiScaling = new System.Windows.Forms.CheckBox();
chkCheckForUpdates = new System.Windows.Forms.CheckBox();
chkMinimizeToSystemTray = new System.Windows.Forms.CheckBox();
chkMinimizeOnClose = new System.Windows.Forms.CheckBox();
chkStartMinimized = new System.Windows.Forms.CheckBox();
chkStartAfterLogin = new System.Windows.Forms.CheckBox();
tabInfo = new System.Windows.Forms.TabPage();
tabOptions = new System.Windows.Forms.TabPage();
btnUpdate = new System.Windows.Forms.Button();
mnuColorProfiles = new System.Windows.Forms.ContextMenuStrip(components);
miCreateHDRColorProfile = new System.Windows.Forms.ToolStripMenuItem();
tcMain.SuspendLayout();
tabOptions.SuspendLayout();
grpOptionsModules.SuspendLayout();
grpMiscellaneousOptions.SuspendLayout();
grpGeneralOptions.SuspendLayout();
mnuColorProfiles.SuspendLayout();
SuspendLayout();
//
// tcMain
@@ -87,9 +53,6 @@
//
// tabOptions
//
tabOptions.Controls.Add(grpOptionsModules);
tabOptions.Controls.Add(grpMiscellaneousOptions);
tabOptions.Controls.Add(grpGeneralOptions);
tabOptions.Location = new System.Drawing.Point(4, 24);
tabOptions.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
tabOptions.Name = "tabOptions";
@@ -99,353 +62,6 @@
tabOptions.Text = "Options";
tabOptions.UseVisualStyleBackColor = true;
//
// grpOptionsModules
//
grpOptionsModules.Controls.Add(label1);
grpOptionsModules.Controls.Add(chkModules);
grpOptionsModules.Location = new System.Drawing.Point(11, 223);
grpOptionsModules.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
grpOptionsModules.Name = "grpOptionsModules";
grpOptionsModules.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
grpOptionsModules.Size = new System.Drawing.Size(658, 131);
grpOptionsModules.TabIndex = 11;
grpOptionsModules.TabStop = false;
grpOptionsModules.Text = "Modules";
//
// label1
//
label1.AutoSize = true;
label1.Location = new System.Drawing.Point(7, 19);
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
label1.Name = "label1";
label1.Size = new System.Drawing.Size(511, 15);
label1.TabIndex = 7;
label1.Text = "Here you can disable modules you do not want. A restart is required for a change to have effect.";
//
// chkModules
//
chkModules.ColumnWidth = 200;
chkModules.FormattingEnabled = true;
chkModules.Location = new System.Drawing.Point(7, 40);
chkModules.MultiColumn = true;
chkModules.Name = "chkModules";
chkModules.Size = new System.Drawing.Size(507, 76);
chkModules.TabIndex = 0;
chkModules.ItemCheck += chkModules_ItemCheck;
//
// grpMiscellaneousOptions
//
grpMiscellaneousOptions.Controls.Add(btnOptionsColorProfiles);
grpMiscellaneousOptions.Controls.Add(btnSetShortcutScreenSaver);
grpMiscellaneousOptions.Controls.Add(label11);
grpMiscellaneousOptions.Controls.Add(edtBlankScreenSaverShortcut);
grpMiscellaneousOptions.Controls.Add(lblFixChromeFontsDescription);
grpMiscellaneousOptions.Controls.Add(chkFixChromeFonts);
grpMiscellaneousOptions.Location = new System.Drawing.Point(11, 360);
grpMiscellaneousOptions.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
grpMiscellaneousOptions.Name = "grpMiscellaneousOptions";
grpMiscellaneousOptions.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
grpMiscellaneousOptions.Size = new System.Drawing.Size(658, 131);
grpMiscellaneousOptions.TabIndex = 5;
grpMiscellaneousOptions.TabStop = false;
grpMiscellaneousOptions.Text = "Miscellaneous";
//
// btnOptionsColorProfiles
//
btnOptionsColorProfiles.Location = new System.Drawing.Point(538, 17);
btnOptionsColorProfiles.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
btnOptionsColorProfiles.Name = "btnOptionsColorProfiles";
btnOptionsColorProfiles.Size = new System.Drawing.Size(112, 27);
btnOptionsColorProfiles.TabIndex = 12;
btnOptionsColorProfiles.Text = "Color Profiles...";
btnOptionsColorProfiles.UseVisualStyleBackColor = true;
btnOptionsColorProfiles.Click += btnOptionsColorProfiles_Click;
//
// btnSetShortcutScreenSaver
//
btnSetShortcutScreenSaver.Location = new System.Drawing.Point(181, 98);
btnSetShortcutScreenSaver.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
btnSetShortcutScreenSaver.Name = "btnSetShortcutScreenSaver";
btnSetShortcutScreenSaver.Size = new System.Drawing.Size(40, 27);
btnSetShortcutScreenSaver.TabIndex = 10;
btnSetShortcutScreenSaver.Text = "Set";
btnSetShortcutScreenSaver.UseVisualStyleBackColor = true;
btnSetShortcutScreenSaver.Click += btnSetShortcutScreenSaver_Click;
//
// label11
//
label11.AutoSize = true;
label11.Location = new System.Drawing.Point(7, 80);
label11.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
label11.Name = "label11";
label11.Size = new System.Drawing.Size(238, 15);
label11.TabIndex = 9;
label11.Text = "Set a shortcut to start the blank screensaver:";
//
// edtBlankScreenSaverShortcut
//
edtBlankScreenSaverShortcut.Location = new System.Drawing.Point(7, 101);
edtBlankScreenSaverShortcut.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
edtBlankScreenSaverShortcut.Name = "edtBlankScreenSaverShortcut";
edtBlankScreenSaverShortcut.ReadOnly = true;
edtBlankScreenSaverShortcut.Size = new System.Drawing.Size(165, 23);
edtBlankScreenSaverShortcut.TabIndex = 7;
edtBlankScreenSaverShortcut.KeyDown += edtShortcut_KeyDown;
edtBlankScreenSaverShortcut.KeyUp += edtShortcut_KeyUp;
//
// lblFixChromeFontsDescription
//
lblFixChromeFontsDescription.AutoSize = true;
lblFixChromeFontsDescription.Location = new System.Drawing.Point(4, 45);
lblFixChromeFontsDescription.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
lblFixChromeFontsDescription.Name = "lblFixChromeFontsDescription";
lblFixChromeFontsDescription.Size = new System.Drawing.Size(427, 15);
lblFixChromeFontsDescription.TabIndex = 6;
lblFixChromeFontsDescription.Text = "This will add the parameter --disable-lcd-text to Chrome and requires elevation.";
//
// chkFixChromeFonts
//
chkFixChromeFonts.AutoSize = true;
chkFixChromeFonts.Location = new System.Drawing.Point(7, 22);
chkFixChromeFonts.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkFixChromeFonts.Name = "chkFixChromeFonts";
chkFixChromeFonts.Size = new System.Drawing.Size(378, 19);
chkFixChromeFonts.TabIndex = 4;
chkFixChromeFonts.Text = "ClearType: fix bad fonts in Chrome (turn on grayscale anti-aliasing)";
chkFixChromeFonts.UseVisualStyleBackColor = true;
chkFixChromeFonts.CheckedChanged += chkFixChromeFonts_CheckedChanged;
//
// grpGeneralOptions
//
grpGeneralOptions.Controls.Add(btnOptionsLog);
grpGeneralOptions.Controls.Add(btnOptionsAdvanced);
grpGeneralOptions.Controls.Add(chkOptionsUseDarkMode);
grpGeneralOptions.Controls.Add(chkAutoInstallUpdates);
grpGeneralOptions.Controls.Add(btnStartStopService);
grpGeneralOptions.Controls.Add(btnElevationInfo);
grpGeneralOptions.Controls.Add(rbElevationService);
grpGeneralOptions.Controls.Add(rbElevationProcess);
grpGeneralOptions.Controls.Add(rbElevationAdmin);
grpGeneralOptions.Controls.Add(label8);
grpGeneralOptions.Controls.Add(rbElevationNone);
grpGeneralOptions.Controls.Add(chkGdiScaling);
grpGeneralOptions.Controls.Add(chkCheckForUpdates);
grpGeneralOptions.Controls.Add(chkMinimizeToSystemTray);
grpGeneralOptions.Controls.Add(chkMinimizeOnClose);
grpGeneralOptions.Controls.Add(chkStartMinimized);
grpGeneralOptions.Controls.Add(chkStartAfterLogin);
grpGeneralOptions.Location = new System.Drawing.Point(7, 6);
grpGeneralOptions.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
grpGeneralOptions.Name = "grpGeneralOptions";
grpGeneralOptions.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
grpGeneralOptions.Size = new System.Drawing.Size(662, 211);
grpGeneralOptions.TabIndex = 2;
grpGeneralOptions.TabStop = false;
grpGeneralOptions.Text = "General";
//
// btnOptionsLog
//
btnOptionsLog.Location = new System.Drawing.Point(103, 172);
btnOptionsLog.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
btnOptionsLog.Name = "btnOptionsLog";
btnOptionsLog.Size = new System.Drawing.Size(88, 27);
btnOptionsLog.TabIndex = 68;
btnOptionsLog.Text = "Logging...";
btnOptionsLog.UseVisualStyleBackColor = true;
btnOptionsLog.Click += btnOptionsLog_Click;
//
// btnOptionsAdvanced
//
btnOptionsAdvanced.Location = new System.Drawing.Point(7, 172);
btnOptionsAdvanced.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
btnOptionsAdvanced.Name = "btnOptionsAdvanced";
btnOptionsAdvanced.Size = new System.Drawing.Size(88, 27);
btnOptionsAdvanced.TabIndex = 11;
btnOptionsAdvanced.Text = "Advanced...";
btnOptionsAdvanced.UseVisualStyleBackColor = true;
btnOptionsAdvanced.Click += btnOptionsAdvanced_Click;
//
// chkOptionsUseDarkMode
//
chkOptionsUseDarkMode.AutoSize = true;
chkOptionsUseDarkMode.Location = new System.Drawing.Point(216, 47);
chkOptionsUseDarkMode.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkOptionsUseDarkMode.Name = "chkOptionsUseDarkMode";
chkOptionsUseDarkMode.Size = new System.Drawing.Size(164, 19);
chkOptionsUseDarkMode.TabIndex = 67;
chkOptionsUseDarkMode.Text = "Dark Mode (experimental)";
chkOptionsUseDarkMode.UseVisualStyleBackColor = true;
chkOptionsUseDarkMode.CheckedChanged += chkOptionsUseDarkMode_CheckedChanged;
//
// chkAutoInstallUpdates
//
chkAutoInstallUpdates.AutoSize = true;
chkAutoInstallUpdates.Location = new System.Drawing.Point(7, 147);
chkAutoInstallUpdates.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkAutoInstallUpdates.Name = "chkAutoInstallUpdates";
chkAutoInstallUpdates.Size = new System.Drawing.Size(179, 19);
chkAutoInstallUpdates.TabIndex = 66;
chkAutoInstallUpdates.Text = "Automatically install updates";
chkAutoInstallUpdates.UseVisualStyleBackColor = true;
chkAutoInstallUpdates.CheckedChanged += chkAutoInstallUpdates_CheckedChanged;
//
// btnStartStopService
//
btnStartStopService.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
btnStartStopService.Location = new System.Drawing.Point(614, 129);
btnStartStopService.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
btnStartStopService.Name = "btnStartStopService";
btnStartStopService.Size = new System.Drawing.Size(40, 27);
btnStartStopService.TabIndex = 65;
btnStartStopService.Text = "Start";
btnStartStopService.UseVisualStyleBackColor = true;
btnStartStopService.Click += btnStartStopService_Click;
//
// btnElevationInfo
//
btnElevationInfo.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
btnElevationInfo.Location = new System.Drawing.Point(329, 67);
btnElevationInfo.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
btnElevationInfo.Name = "btnElevationInfo";
btnElevationInfo.Size = new System.Drawing.Size(22, 27);
btnElevationInfo.TabIndex = 64;
btnElevationInfo.Text = "?";
btnElevationInfo.UseVisualStyleBackColor = true;
btnElevationInfo.Click += btnElevationInfo_Click;
//
// rbElevationService
//
rbElevationService.AutoSize = true;
rbElevationService.Location = new System.Drawing.Point(216, 133);
rbElevationService.Name = "rbElevationService";
rbElevationService.Size = new System.Drawing.Size(136, 19);
rbElevationService.TabIndex = 63;
rbElevationService.TabStop = true;
rbElevationService.Tag = "2";
rbElevationService.Text = "Use Windows Service";
rbElevationService.UseVisualStyleBackColor = true;
rbElevationService.CheckedChanged += rbElevationNone_CheckedChanged;
//
// rbElevationProcess
//
rbElevationProcess.AutoSize = true;
rbElevationProcess.Location = new System.Drawing.Point(216, 154);
rbElevationProcess.Name = "rbElevationProcess";
rbElevationProcess.Size = new System.Drawing.Size(189, 19);
rbElevationProcess.TabIndex = 62;
rbElevationProcess.TabStop = true;
rbElevationProcess.Tag = "3";
rbElevationProcess.Text = "Use dedicated elevated process";
rbElevationProcess.UseVisualStyleBackColor = true;
rbElevationProcess.CheckedChanged += rbElevationNone_CheckedChanged;
//
// rbElevationAdmin
//
rbElevationAdmin.AutoSize = true;
rbElevationAdmin.Location = new System.Drawing.Point(216, 112);
rbElevationAdmin.Name = "rbElevationAdmin";
rbElevationAdmin.Size = new System.Drawing.Size(97, 19);
rbElevationAdmin.TabIndex = 61;
rbElevationAdmin.TabStop = true;
rbElevationAdmin.Tag = "1";
rbElevationAdmin.Text = "Run as admin";
rbElevationAdmin.UseVisualStyleBackColor = true;
rbElevationAdmin.CheckedChanged += rbElevationNone_CheckedChanged;
//
// label8
//
label8.AutoSize = true;
label8.Location = new System.Drawing.Point(216, 73);
label8.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
label8.Name = "label8";
label8.Size = new System.Drawing.Size(105, 15);
label8.TabIndex = 60;
label8.Text = "Elevation-method:";
//
// rbElevationNone
//
rbElevationNone.AutoSize = true;
rbElevationNone.Location = new System.Drawing.Point(216, 91);
rbElevationNone.Name = "rbElevationNone";
rbElevationNone.Size = new System.Drawing.Size(54, 19);
rbElevationNone.TabIndex = 10;
rbElevationNone.TabStop = true;
rbElevationNone.Tag = "0";
rbElevationNone.Text = "None";
rbElevationNone.UseVisualStyleBackColor = true;
rbElevationNone.CheckedChanged += rbElevationNone_CheckedChanged;
//
// chkGdiScaling
//
chkGdiScaling.AutoSize = true;
chkGdiScaling.Location = new System.Drawing.Point(216, 22);
chkGdiScaling.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkGdiScaling.Name = "chkGdiScaling";
chkGdiScaling.Size = new System.Drawing.Size(197, 19);
chkGdiScaling.TabIndex = 7;
chkGdiScaling.Text = "Use GDI Scaling (requires restart)";
chkGdiScaling.UseVisualStyleBackColor = true;
chkGdiScaling.CheckedChanged += chkGdiScaling_CheckedChanged;
//
// chkCheckForUpdates
//
chkCheckForUpdates.AutoSize = true;
chkCheckForUpdates.Location = new System.Drawing.Point(7, 122);
chkCheckForUpdates.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkCheckForUpdates.Name = "chkCheckForUpdates";
chkCheckForUpdates.Size = new System.Drawing.Size(197, 19);
chkCheckForUpdates.TabIndex = 6;
chkCheckForUpdates.Text = "Automatically check for updates";
chkCheckForUpdates.UseVisualStyleBackColor = true;
chkCheckForUpdates.CheckedChanged += chkCheckForUpdates_CheckedChanged;
//
// chkMinimizeToSystemTray
//
chkMinimizeToSystemTray.AutoSize = true;
chkMinimizeToSystemTray.Location = new System.Drawing.Point(7, 97);
chkMinimizeToSystemTray.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkMinimizeToSystemTray.Name = "chkMinimizeToSystemTray";
chkMinimizeToSystemTray.Size = new System.Drawing.Size(152, 19);
chkMinimizeToSystemTray.TabIndex = 5;
chkMinimizeToSystemTray.Text = "Minimize to system tray";
chkMinimizeToSystemTray.UseVisualStyleBackColor = true;
chkMinimizeToSystemTray.CheckedChanged += chkMinimizeToSystemTray_CheckedChanged;
//
// chkMinimizeOnClose
//
chkMinimizeOnClose.AutoSize = true;
chkMinimizeOnClose.Location = new System.Drawing.Point(7, 72);
chkMinimizeOnClose.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkMinimizeOnClose.Name = "chkMinimizeOnClose";
chkMinimizeOnClose.Size = new System.Drawing.Size(122, 19);
chkMinimizeOnClose.TabIndex = 4;
chkMinimizeOnClose.Text = "Minimize on close";
chkMinimizeOnClose.UseVisualStyleBackColor = true;
chkMinimizeOnClose.CheckedChanged += chkMinimizeOnClose_CheckedChanged;
//
// chkStartMinimized
//
chkStartMinimized.AutoSize = true;
chkStartMinimized.Location = new System.Drawing.Point(7, 47);
chkStartMinimized.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkStartMinimized.Name = "chkStartMinimized";
chkStartMinimized.Size = new System.Drawing.Size(109, 19);
chkStartMinimized.TabIndex = 3;
chkStartMinimized.Text = "Start minimized";
chkStartMinimized.UseVisualStyleBackColor = true;
//
// chkStartAfterLogin
//
chkStartAfterLogin.AutoSize = true;
chkStartAfterLogin.Location = new System.Drawing.Point(7, 22);
chkStartAfterLogin.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
chkStartAfterLogin.Name = "chkStartAfterLogin";
chkStartAfterLogin.Size = new System.Drawing.Size(183, 19);
chkStartAfterLogin.TabIndex = 2;
chkStartAfterLogin.Text = "Automatically start after login";
chkStartAfterLogin.UseVisualStyleBackColor = true;
chkStartAfterLogin.CheckedChanged += chkStartAfterLogin_CheckedChanged;
//
// tabInfo
//
tabInfo.Location = new System.Drawing.Point(4, 24);
@@ -472,20 +88,6 @@
btnUpdate.Visible = false;
btnUpdate.Click += btnUpdate_Click;
//
// mnuColorProfiles
//
mnuColorProfiles.ImageScalingSize = new System.Drawing.Size(20, 20);
mnuColorProfiles.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { miCreateHDRColorProfile });
mnuColorProfiles.Name = "mnuLgButtons";
mnuColorProfiles.Size = new System.Drawing.Size(214, 26);
//
// miCreateHDRColorProfile
//
miCreateHDRColorProfile.Name = "miCreateHDRColorProfile";
miCreateHDRColorProfile.Size = new System.Drawing.Size(213, 22);
miCreateHDRColorProfile.Text = "Create HDR Color Profile...";
miCreateHDRColorProfile.Click += miCreateHDRColorProfile_Click;
//
// MainForm
//
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
@@ -494,7 +96,6 @@
ClientSize = new System.Drawing.Size(1150, 594);
Controls.Add(btnUpdate);
Controls.Add(tcMain);
Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon");
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
MinimumSize = new System.Drawing.Size(1140, 611);
Name = "MainForm";
@@ -502,8 +103,6 @@
Activated += MainForm_Activated;
Deactivate += MainForm_Deactivate;
FormClosing += MainForm_FormClosing;
FormClosed += MainForm_FormClosed;
Load += MainForm_Load;
Shown += MainForm_Shown;
ResizeBegin += MainForm_ResizeBegin;
ResizeEnd += MainForm_ResizeEnd;
@@ -511,13 +110,6 @@
Resize += MainForm_Resize;
tcMain.ResumeLayout(false);
tabOptions.ResumeLayout(false);
grpOptionsModules.ResumeLayout(false);
grpOptionsModules.PerformLayout();
grpMiscellaneousOptions.ResumeLayout(false);
grpMiscellaneousOptions.PerformLayout();
grpGeneralOptions.ResumeLayout(false);
grpGeneralOptions.PerformLayout();
mnuColorProfiles.ResumeLayout(false);
ResumeLayout(false);
}
@@ -525,38 +117,8 @@
private System.Windows.Forms.TabControl tcMain;
private System.Windows.Forms.TabPage tabOptions;
private System.Windows.Forms.GroupBox grpGeneralOptions;
private System.Windows.Forms.CheckBox chkStartMinimized;
private System.Windows.Forms.CheckBox chkStartAfterLogin;
private System.Windows.Forms.TabPage tabInfo;
private System.Windows.Forms.GroupBox grpMiscellaneousOptions;
private System.Windows.Forms.CheckBox chkFixChromeFonts;
private System.Windows.Forms.Label lblFixChromeFontsDescription;
private System.Windows.Forms.Button btnSetShortcutScreenSaver;
private System.Windows.Forms.Label label11;
private System.Windows.Forms.TextBox edtBlankScreenSaverShortcut;
private System.Windows.Forms.CheckBox chkMinimizeOnClose;
private System.Windows.Forms.CheckBox chkMinimizeToSystemTray;
private System.Windows.Forms.CheckBox chkCheckForUpdates;
private System.Windows.Forms.CheckBox chkGdiScaling;
private System.Windows.Forms.RadioButton rbElevationService;
private System.Windows.Forms.RadioButton rbElevationProcess;
private System.Windows.Forms.RadioButton rbElevationAdmin;
private System.Windows.Forms.Label label8;
private System.Windows.Forms.RadioButton rbElevationNone;
private System.Windows.Forms.Button btnElevationInfo;
private System.Windows.Forms.Button btnStartStopService;
private System.Windows.Forms.CheckBox chkAutoInstallUpdates;
private System.Windows.Forms.Button btnUpdate;
private System.Windows.Forms.GroupBox grpOptionsModules;
private System.Windows.Forms.CheckedListBox chkModules;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.CheckBox chkOptionsUseDarkMode;
private System.Windows.Forms.Button btnOptionsAdvanced;
private System.Windows.Forms.Button btnOptionsLog;
private System.Windows.Forms.Button btnOptionsColorProfiles;
private System.Windows.Forms.ContextMenuStrip mnuColorProfiles;
private System.Windows.Forms.ToolStripMenuItem miCreateHDRColorProfile;
}
}
+69 -1167
View File
File diff suppressed because it is too large. Load diff
+27 -1806
View File
File diff suppressed because it is too large. Load diff
+196
View File
@@ -0,0 +1,196 @@
using ColorControl.Services.Common;
using ColorControl.Shared.Common;
using ColorControl.Shared.Contracts;
using ColorControl.Shared.EventDispatcher;
using ColorControl.Shared.Forms;
using ColorControl.Shared.Native;
using ColorControl.Shared.Services;
using NLog;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace ColorControl;
internal class MainWorker
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly BackgroundWorker _backgroundWorker;
private readonly GlobalContext _globalContext;
private readonly ServiceManager _serviceManager;
private readonly Config _config;
private readonly NotifyIconManager _notifyIconManager;
private readonly WindowMessageDispatcher _windowMessageDispatcher;
private readonly KeyboardShortcutDispatcher _keyboardShortcutDispatcher;
private readonly PowerEventDispatcher _powerEventDispatcher;
private readonly WinApiService _winApiService;
private readonly WinApiAdminService _winApiAdminService;
private readonly UpdateManager _updateManager;
private nint _screenStateNotify;
public static int SHORTCUTID_SCREENSAVER = -100;
public MainWorker(
GlobalContext globalContext,
ServiceManager serviceManager,
NotifyIconManager notifyIconManager,
WindowMessageDispatcher windowMessageDispatcher,
KeyboardShortcutDispatcher keyboardShortcutDispatcher,
PowerEventDispatcher powerEventDispatcher,
WinApiService winApiService,
WinApiAdminService winApiAdminService,
UpdateManager updateManager)
{
_backgroundWorker = new BackgroundWorker();
_globalContext = globalContext;
_serviceManager = serviceManager;
_notifyIconManager = notifyIconManager;
_windowMessageDispatcher = windowMessageDispatcher;
_keyboardShortcutDispatcher = keyboardShortcutDispatcher;
_powerEventDispatcher = powerEventDispatcher;
_winApiService = winApiService;
_winApiAdminService = winApiAdminService;
_updateManager = updateManager;
_config = _globalContext.Config;
}
public async Task DoWork()
{
if (_backgroundWorker.CancellationPending)
{
return;
}
await Init();
System.Windows.Forms.Application.Run(_windowMessageDispatcher.MessageForm);
_serviceManager.Save();
_notifyIconManager.HideIcon();
if (_screenStateNotify != 0)
{
WinApi.UnregisterPowerSettingNotification(_screenStateNotify);
_screenStateNotify = 0;
}
}
private async Task Init()
{
_globalContext.SynchronizationContext = AsyncOperationManager.SynchronizationContext;
_windowMessageDispatcher.RegisterEventHandler(WindowMessageDispatcher.Event_WindowMessageQueryEndSession, HandleQueryEndSessionEvent);
_windowMessageDispatcher.RegisterEventHandler(WindowMessageDispatcher.Event_WindowMessagePowerBroadcast, HandlePowerBroadcastEvent);
_windowMessageDispatcher.RegisterEventHandler(WindowMessageDispatcher.Event_WindowMessageShowWindow, HandleShowWindowEvent);
_windowMessageDispatcher.RegisterEventHandler(WindowMessageDispatcher.Event_WindowMessageUserBringToFront, HandleUserBringToFrontEvent);
_screenStateNotify = WinApi.RegisterPowerSettingNotification(_windowMessageDispatcher.MessageForm.Handle, ref Utils.GUID_CONSOLE_DISPLAY_STATE, 0);
_keyboardShortcutDispatcher.SetUseRawInput(_config.UseRawInput);
MessageForms.KeyboardShortcutDispatcher = _keyboardShortcutDispatcher;
_notifyIconManager.Build();
_serviceManager.LoadModules();
// Auto-apply fix if desired but not currently installed
if (_config.FixChromeFonts && _winApiService.IsChromeInstalled() && !_winApiService.IsChromeFixInstalled())
{
_winApiAdminService.InstallChromeFix(true, Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));
}
InitTheme();
_keyboardShortcutDispatcher.RegisterShortcut(SHORTCUTID_SCREENSAVER, _config.ScreenSaverShortcut);
_keyboardShortcutDispatcher.RegisterEventHandler(KeyboardShortcutDispatcher.Event_HotKey, HandleHotKeyEvent);
if (_config.UiType != UiType.WinForms)
{
await Program.StartUiServer();
}
}
private bool _startupSent = false;
private void HandleShowWindowEvent(object sender, WindowMessageEventArgs e)
{
if (_startupSent)
{
return;
}
_startupSent = true;
_ = _powerEventDispatcher.SendEventAsync(PowerEventDispatcher.Event_Startup);
if (_config.CheckForUpdates)
{
_ = _updateManager.CheckForUpdatesAndInstall();
}
if (!_config.StartMinimized /*|| Debugger.IsAttached*/)
{
Program.OpenDefaultUi();
}
}
private void HandleUserBringToFrontEvent(object sender, WindowMessageEventArgs e)
{
Program.OpenDefaultUi();
}
private void HandlePowerBroadcastEvent(object sender, WindowMessageEventArgs e)
{
if (e.Message.WParam.ToInt32() == WinApi.PBT_POWERSETTINGCHANGE)
{
var ps = Marshal.PtrToStructure<WinApi.POWERBROADCAST_SETTING>(e.Message.LParam);
var power = (WindowsMonitorPowerSetting)ps.Data;
Logger.Debug($"PBT_POWERSETTINGCHANGE: {power}");
_powerEventDispatcher.SendEvent(power == WindowsMonitorPowerSetting.Off ? PowerEventDispatcher.Event_MonitorOff : PowerEventDispatcher.Event_MonitorOn);
}
}
private void HandleQueryEndSessionEvent(object sender, WindowMessageEventArgs e)
{
Logger.Debug($"MainWorker: QueryEndSession");
Logger.Debug($"MainWorker: SystemShutdown");
_serviceManager.Save();
_powerEventDispatcher.SendEvent(PowerEventDispatcher.Event_Shutdown);
}
private void HandleHotKeyEvent(object sender, KeyboardShortcutEventArgs args)
{
if (args.HotKeyId == SHORTCUTID_SCREENSAVER)
{
StartScreenSaver();
}
}
private void StartScreenSaver()
{
//NativeMethods.SendMessageW(-1, NativeConstants.WM_SYSCOMMAND, NativeConstants.SC_MONITORPOWER, (int)WinApi.MonitorState.OFF);
var screenSaver = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "scrnsave.scr");
Process.Start("explorer.exe", screenSaver);
}
private void InitTheme()
{
DarkModeUtils.SetDarkMode(_config.UseDarkMode);
WinApi.RefreshImmersiveColorPolicyState();
DarkModeUtils.InitWpfTheme();
DarkModeUtils.SetContextMenuForeColor(_notifyIconManager.NotifyIcon.ContextMenuStrip, FormUtils.CurrentForeColor);
}
}
+184 -34
View File
@@ -1,12 +1,14 @@
using ColorControl.Forms;
using ColorControl.Services.AMD;
using ColorControl.Services.Common;
using ColorControl.Services.EventDispatcher;
using ColorControl.Services.GameLauncher;
using ColorControl.Services.LG;
using ColorControl.Services.NVIDIA;
using ColorControl.Services.Samsung;
using ColorControl.Shared.Common;
using ColorControl.Shared.Contracts;
using ColorControl.Shared.EventDispatcher;
using ColorControl.Shared.Forms;
using ColorControl.Shared.Native;
using ColorControl.Shared.Services;
using ColorControl.Svc;
@@ -41,9 +43,13 @@ namespace ColorControl
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private static Mutex _mutex;
private static MainForm _mainForm;
public static string MutexId;
public static MainForm _mainForm;
private static LoggingRule _loggingRule;
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread] // autogenerated
public static void Main(string[] args)
{
@@ -51,10 +57,6 @@ namespace ColorControl
MainAsync(args).GetAwaiter().GetResult();
}
/// <summary>
/// The main entry point for the application.
/// </summary>
//[STAThread]
static async Task MainAsync(string[] args)
{
DataDir = Utils.GetDataPath();
@@ -73,11 +75,12 @@ namespace ColorControl
return;
}
var host = CreateHostBuilder().Build();
ServiceProvider = host.Services;
var mutexId = $"Global\\{typeof(MainForm).GUID}";
await RunApp(args);
}
static async Task RunApp(string[] args)
{
var startTime = DateTime.Now;
var currentDomain = AppDomain.CurrentDomain;
// Handler for unhandled exceptions.
currentDomain.UnhandledException += GlobalUnhandledExceptionHandler;
@@ -86,17 +89,25 @@ namespace ColorControl
LoadConfig();
var appContextProvider = ServiceProvider.GetRequiredService<AppContextProvider>();
var startUpParams = StartUpParams.Parse(args);
AppContext = new GlobalContext(Config, startUpParams, DataDir, _loggingRule, mutexId);
appContextProvider.SetAppContext(AppContext);
if (Config.UseGdiScaling)
{
Application.SetHighDpiMode(HighDpiMode.DpiUnawareGdiScaled);
var result = Application.SetHighDpiMode(HighDpiMode.DpiUnawareGdiScaled);
Logger.Debug($"Result of setting SetHighDpiMode: {result}");
}
MutexId = $"Global\\{typeof(MainForm).GUID}";
var startUpParams = StartUpParams.Parse(args);
AppContext = new GlobalContext(Config, startUpParams, DataDir, _loggingRule, null, startTime, MutexId);
var host = CreateHostBuilder().Build();
ServiceProvider = host.Services;
AppContext.ServiceProvider = ServiceProvider;
MessageForms.Title = AppContext.ApplicationTitle;
var existingProcess = Utils.GetProcessByName("ColorControl");
try
@@ -131,19 +142,12 @@ namespace ColorControl
_mutex.WaitOne();
try
{
//if (Debugger.IsAttached)
//{
// Utils.StartService();
//}
//var result = WinApi.SetPreferredAppMode(1);
//result = WinApi.RefreshImmersiveColorPolicyState();
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
if (Debugger.IsAttached)
{
//Utils.StartService();
var winApiService = ServiceProvider.GetRequiredService<WinApiService>();
if (winApiService.IsAdministrator())
@@ -151,18 +155,21 @@ namespace ColorControl
var backgroundService = ServiceProvider.GetRequiredService<ColorControlBackgroundService>();
backgroundService.PipeName = PipeUtils.ElevatedPipe;
Task.Run(async () => await backgroundService.StartAsync(CancellationToken.None));
_ = Task.Run(async () => await backgroundService.StartAsync(CancellationToken.None));
}
}
_mainForm = ServiceProvider.GetRequiredService<MainForm>();
Application.Run(_mainForm);
var worker = ServiceProvider.GetRequiredService<MainWorker>();
await worker.DoWork();
if (Debugger.IsAttached && !IsRestarting)
{
var winApiAdminService = ServiceProvider.GetRequiredService<WinApiAdminService>();
winApiAdminService.StopService();
}
await BlazorUiManager.Stop();
}
catch (Exception ex)
{
@@ -179,6 +186,40 @@ namespace ColorControl
}
}
public static void OpenDefaultUi()
{
var _ = Config.UiType switch
{
UiType.WebDefaultBrowser => OpenNewUi(),
UiType.WebEmbedded => OpenNewUiEmbedded(),
_ => OpenMainForm()
};
}
public static Task OpenMainForm()
{
_mainForm ??= ServiceProvider.GetRequiredService<MainForm>();
_mainForm.OpenForm();
return Task.CompletedTask;
}
public static bool IsMainFormOpened()
{
return _mainForm != null;
}
public static void Exit()
{
UserExit = true;
_mainForm?.Close();
if (AppContext.MainHandle != 0)
{
NativeMethods.SendMessageW(AppContext.MainHandle, NativeConstants.WM_CLOSE, 0, 0);
}
}
public static void Restart()
{
IsRestarting = true;
@@ -192,6 +233,65 @@ namespace ColorControl
Environment.Exit(0);
}
public static async Task OpenNewUi()
{
await OpenNewUi(false);
}
public static async Task OpenNewUiEmbedded()
{
await OpenNewUi(true);
}
public static async Task OpenNewUi(bool embedded)
{
if (await StartUiServer())
{
await Task.Delay(1000);
}
if (BlazorUiManager.GetCurrentPort(Config) <= 0)
{
await OpenMainForm();
return;
}
if (embedded)
{
BrowserWindow.CreateAndShow(BlazorUiManager.GetCurrentUrl(Config));
}
else
{
var winApi = AppContext.ServiceProvider.GetRequiredService<WinApiAdminService>();
winApi.StartProcess(BlazorUiManager.GetCurrentUrl(Config));
}
}
public static async Task<bool> StartOrStopUiServer()
{
if (Config.UiType == UiType.WinForms)
{
var _ = Task.Run(() => BlazorUiManager.Stop());
return true;
}
return await StartUiServer();
}
public static async Task<bool> StartUiServer()
{
var backgroundService = ServiceProvider.GetRequiredService<ColorControlBackgroundService>();
if (backgroundService.PipeName != PipeUtils.MainPipe)
{
backgroundService.PipeName = PipeUtils.MainPipe;
await backgroundService.StartAsync(CancellationToken.None);
}
return await BlazorUiManager.Start(Config);
}
private static void InitLogger(bool runAsService, bool runElevated)
{
var config = new LoggingConfiguration();
@@ -248,23 +348,45 @@ namespace ColorControl
{
//services.AddSingleton<NvService>();
services.RegisterSharedServices();
services.AddSingleton<MainWorker>();
services.AddSingleton<NotifyIconManager>();
services.AddSingleton<LgService>();
services.AddSingleton<GameService>();
services.AddSingleton<SamsungService>();
services.AddSingleton<AmdService>();
services.AddSingleton<PowerEventDispatcher>();
services.AddSingleton<ProcessEventDispatcher>();
services.AddSingleton<WindowMessageDispatcher>();
services.AddSingleton<KeyboardShortcutDispatcher>();
services.AddSingleton<DisplayChangeEventDispatcher>();
services.AddSingleton<MainForm>();
services.AddSingleton<ElevatedForm>();
services.AddSingleton<LogWindow>();
services.AddSingleton<BrowserWindow>();
services.AddTransient<ColorProfileWindow>();
services.AddSingleton<RestartDetector>();
services.AddSingleton<InfoPanel>();
services.AddSingleton<OptionsPanel>();
services.AddSingleton<ElevationService>();
services.AddSingleton<UpdateManager>();
services.AddSingleton<OptionsService>();
services.AddSingleton<LoggingService>();
services.AddSingleton<AmdPanel>();
services.AddSingleton<NvPanel>();
services.AddSingleton<LgPanel>();
services.AddSingleton<SamsungPanel>();
services.AddSingleton<GamePanel>();
});
}
private static async Task RunService(string[] args)
{
var startTime = DateTime.Now;
Logger.Debug("RUNNING SERVICE");
AppContext = new GlobalContext(null, new StartUpParams(), DataDir, _loggingRule, null, startTime);
using IHost host = Host.CreateDefaultBuilder(args)
.UseWindowsService(options =>
{
@@ -273,22 +395,20 @@ namespace ColorControl
.ConfigureServices(services =>
{
services.RegisterSharedServices();
services.AddSingleton<PowerEventDispatcher>();
services.AddHostedService<ColorControlBackgroundService>();
})
.Build();
ServiceProvider = host.Services;
var appContextProvider = ServiceProvider.GetRequiredService<AppContextProvider>();
AppContext = new GlobalContext(null, new StartUpParams(), DataDir, _loggingRule);
appContextProvider.SetAppContext(AppContext);
AppContext.ServiceProvider = ServiceProvider;
await host.RunAsync();
}
private static void RegisterSharedServices(this IServiceCollection services)
{
services.AddSingleton<AppContextProvider>();
services.AddSingleton(AppContext);
services.AddSingleton<SessionSwitchDispatcher>();
services.AddSingleton<WinApiAdminService>();
services.AddSingleton<WinApiService>();
@@ -299,8 +419,16 @@ namespace ColorControl
services.AddSingleton<WolService>();
services.AddSingleton<ColorControlBackgroundService>();
services.AddSingleton<ServiceManager>();
services.AddSingleton<ColorProfileService>();
RpcServerService.ServiceTypes.Add(typeof(NvService));
RpcServerService.ServiceTypes.Add(typeof(GameService));
RpcServerService.ServiceTypes.Add(typeof(OptionsService));
RpcServerService.ServiceTypes.Add(typeof(LoggingService));
RpcServerService.ServiceTypes.Add(typeof(SamsungService));
RpcServerService.ServiceTypes.Add(typeof(LgService));
RpcServerService.ServiceTypes.Add(typeof(AmdService));
RpcServerService.ServiceTypes.Add(typeof(ColorProfileService));
}
public static int EnumThreadWindows(IntPtr handle, IntPtr param)
@@ -334,5 +462,27 @@ namespace ColorControl
MessageBox.Show(message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
internal static void SetTheme(bool useDarkMode)
{
DarkModeUtils.SetDarkMode(useDarkMode);
WinApi.RefreshImmersiveColorPolicyState();
DarkModeUtils.InitWpfTheme();
var notifyIconManager = ServiceProvider.GetRequiredService<NotifyIconManager>();
DarkModeUtils.SetContextMenuForeColor(notifyIconManager.NotifyIcon.ContextMenuStrip, FormUtils.CurrentForeColor);
_mainForm?.SetTheme(useDarkMode);
}
internal static NotifyIcon GetNotifyIcon()
{
var notifyIconManager = ServiceProvider.GetRequiredService<NotifyIconManager>();
return notifyIconManager.NotifyIcon;
}
}
}
+15 -5
View File
@@ -22,7 +22,7 @@ namespace ColorControl.Properties {
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
@@ -36,7 +36,7 @@ namespace ColorControl.Properties {
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ColorControl.Properties.Resources", typeof(Resources).Assembly);
@@ -51,7 +51,7 @@ namespace ColorControl.Properties {
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
@@ -60,10 +60,20 @@ namespace ColorControl.Properties {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
/// </summary>
public static System.Drawing.Icon AppIcon {
get {
object obj = ResourceManager.GetObject("AppIcon", resourceCulture);
return ((System.Drawing.Icon)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap LG_remote {
public static System.Drawing.Bitmap LG_remote {
get {
object obj = ResourceManager.GetObject("LG_remote", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
@@ -108,7 +118,7 @@ namespace ColorControl.Properties {
///Rakuten TV;116;1218
///Play;202 [rest of string was truncated]&quot;;.
/// </summary>
internal static string RC_coords {
public static string RC_coords {
get {
return ResourceManager.GetString("RC_coords", resourceCulture);
}
+3
View File
@@ -118,6 +118,9 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="AppIcon" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\pngbarn.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="LG_remote" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\LG_remote.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
+69 -77
View File
@@ -1,77 +1,69 @@
{
"type":"register",
"id":"register_0",
"payload":{
"forcePairing":false,
"pairingType":"PROMPT",
"client-key":"CLIENTKEYGOESHERE",
"manifest":{
"manifestVersion":1,
"appVersion":"1.1",
"signed":{
"created":"20140509",
"appId":"com.lge.test",
"vendorId":"com.lge",
"localizedAppNames":{
"":"LG Remote App",
"ko-KR":"리모컨 앱",
"zxx-XX":"ЛГ Rэмotэ AПП"
},
"localizedVendorNames":{
"":"LG Electronics"
},
"permissions":[
"TEST_SECURE",
"CONTROL_INPUT_TEXT",
"CONTROL_MOUSE_AND_KEYBOARD",
"READ_INSTALLED_APPS",
"READ_LGE_SDX",
"READ_NOTIFICATIONS",
"SEARCH",
"WRITE_SETTINGS",
"WRITE_NOTIFICATION_ALERT",
"CONTROL_POWER",
"READ_CURRENT_CHANNEL",
"READ_RUNNING_APPS",
"READ_UPDATE_INFO",
"UPDATE_FROM_REMOTE_APP",
"READ_LGE_TV_INPUT_EVENTS",
"READ_TV_CURRENT_TIME"
],
"serial":"2f930e2d2cfe083771f68e4fe7bb07"
},
"permissions":[
"LAUNCH",
"LAUNCH_WEBAPP",
"APP_TO_APP",
"CLOSE",
"TEST_OPEN",
"TEST_PROTECTED",
"CONTROL_AUDIO",
"CONTROL_DISPLAY",
"CONTROL_INPUT_JOYSTICK",
"CONTROL_INPUT_MEDIA_RECORDING",
"CONTROL_INPUT_MEDIA_PLAYBACK",
"CONTROL_INPUT_TV",
"CONTROL_POWER",
"CONTROL_TV_SCREEN",
"READ_APP_STATUS",
"READ_CURRENT_CHANNEL",
"READ_INPUT_DEVICE_LIST",
"READ_NETWORK_STATE",
"READ_RUNNING_APPS",
"READ_TV_CHANNEL_LIST",
"WRITE_NOTIFICATION_TOAST",
"READ_POWER_STATE",
"READ_COUNTRY_INFO",
"READ_SETTINGS"
],
"signatures":[
{
"signatureVersion":1,
"signature":"eyJhbGdvcml0aG0iOiJSU0EtU0hBMjU2Iiwia2V5SWQiOiJ0ZXN0LXNpZ25pbmctY2VydCIsInNpZ25hdHVyZVZlcnNpb24iOjF9.hrVRgjCwXVvE2OOSpDZ58hR+59aFNwYDyjQgKk3auukd7pcegmE2CzPCa0bJ0ZsRAcKkCTJrWo5iDzNhMBWRyaMOv5zWSrthlf7G128qvIlpMT0YNY+n/FaOHE73uLrS/g7swl3/qH/BGFG2Hu4RlL48eb3lLKqTt2xKHdCs6Cd4RMfJPYnzgvI4BNrFUKsjkcu+WD4OO2A27Pq1n50cMchmcaXadJhGrOqH5YmHdOCj5NSHzJYrsW0HPlpuAx/ECMeIZYDh6RMqaFM2DXzdKX9NmmyqzJ3o/0lkk/N97gfVRLW5hA29yeAwaCViZNCP8iC9aO0q9fQojoa7NQnAtw=="
}
]
}
}
}
{
"type": "register",
"id": "register_0",
"payload": {
"forcePairing": false,
"pairingType": "PROMPT",
"client-key": "CLIENTKEYGOESHERE",
"manifest": {
"manifestVersion": 1,
"appVersion": "1.0",
"signed": {
"appId": "com.colorcontrol.app",
"created": "20260426",
"localizedAppNames": {
"": "ColorControl"
},
"localizedVendorNames": {
"": "ColorControl"
},
"permissions": [
"CONTROL_POWER",
"READ_POWER_STATE",
"READ_CURRENT_CHANNEL",
"READ_RUNNING_APPS",
"READ_NETWORK_STATE",
"READ_INPUT_DEVICE_LIST",
"READ_TV_CHANNEL_LIST",
"READ_SETTINGS",
"READ_COUNTRY_INFO",
"WRITE_NOTIFICATION_TOAST",
"CONTROL_AUDIO",
"CONTROL_DISPLAY",
"CONTROL_INPUT_JOYSTICK",
"CONTROL_INPUT_MEDIA_PLAYBACK",
"CONTROL_INPUT_TV",
"CONTROL_TV_SCREEN",
"LAUNCH",
"LAUNCH_WEBAPP",
"APP_TO_APP",
"CLOSE"
],
"serial": "colorcontrol-webos26"
},
"permissions": [
"LAUNCH",
"LAUNCH_WEBAPP",
"APP_TO_APP",
"CLOSE",
"CONTROL_AUDIO",
"CONTROL_DISPLAY",
"CONTROL_INPUT_JOYSTICK",
"CONTROL_INPUT_MEDIA_PLAYBACK",
"CONTROL_INPUT_TV",
"CONTROL_POWER",
"CONTROL_TV_SCREEN",
"READ_APP_STATUS",
"READ_CURRENT_CHANNEL",
"READ_INPUT_DEVICE_LIST",
"READ_NETWORK_STATE",
"READ_RUNNING_APPS",
"READ_TV_CHANNEL_LIST",
"WRITE_NOTIFICATION_TOAST",
"READ_POWER_STATE",
"READ_COUNTRY_INFO",
"READ_SETTINGS"
]
}
}
}
@@ -0,0 +1,77 @@
{
"type":"register",
"id":"register_0",
"payload":{
"forcePairing":false,
"pairingType":"PROMPT",
"client-key":"CLIENTKEYGOESHERE",
"manifest":{
"manifestVersion":1,
"appVersion":"1.1",
"signed":{
"created":"20140509",
"appId":"com.lge.test",
"vendorId":"com.lge",
"localizedAppNames":{
"":"LG Remote App",
"ko-KR":"리모컨 앱",
"zxx-XX":"ЛГ Rэмotэ AПП"
},
"localizedVendorNames":{
"":"LG Electronics"
},
"permissions":[
"TEST_SECURE",
"CONTROL_INPUT_TEXT",
"CONTROL_MOUSE_AND_KEYBOARD",
"READ_INSTALLED_APPS",
"READ_LGE_SDX",
"READ_NOTIFICATIONS",
"SEARCH",
"WRITE_SETTINGS",
"WRITE_NOTIFICATION_ALERT",
"CONTROL_POWER",
"READ_CURRENT_CHANNEL",
"READ_RUNNING_APPS",
"READ_UPDATE_INFO",
"UPDATE_FROM_REMOTE_APP",
"READ_LGE_TV_INPUT_EVENTS",
"READ_TV_CURRENT_TIME"
],
"serial":"2f930e2d2cfe083771f68e4fe7bb07"
},
"permissions":[
"LAUNCH",
"LAUNCH_WEBAPP",
"APP_TO_APP",
"CLOSE",
"TEST_OPEN",
"TEST_PROTECTED",
"CONTROL_AUDIO",
"CONTROL_DISPLAY",
"CONTROL_INPUT_JOYSTICK",
"CONTROL_INPUT_MEDIA_RECORDING",
"CONTROL_INPUT_MEDIA_PLAYBACK",
"CONTROL_INPUT_TV",
"CONTROL_POWER",
"CONTROL_TV_SCREEN",
"READ_APP_STATUS",
"READ_CURRENT_CHANNEL",
"READ_INPUT_DEVICE_LIST",
"READ_NETWORK_STATE",
"READ_RUNNING_APPS",
"READ_TV_CHANNEL_LIST",
"WRITE_NOTIFICATION_TOAST",
"READ_POWER_STATE",
"READ_COUNTRY_INFO",
"READ_SETTINGS"
],
"signatures":[
{
"signatureVersion":1,
"signature":"eyJhbGdvcml0aG0iOiJSU0EtU0hBMjU2Iiwia2V5SWQiOiJ0ZXN0LXNpZ25pbmctY2VydCIsInNpZ25hdHVyZVZlcnNpb24iOjF9.hrVRgjCwXVvE2OOSpDZ58hR+59aFNwYDyjQgKk3auukd7pcegmE2CzPCa0bJ0ZsRAcKkCTJrWo5iDzNhMBWRyaMOv5zWSrthlf7G128qvIlpMT0YNY+n/FaOHE73uLrS/g7swl3/qH/BGFG2Hu4RlL48eb3lLKqTt2xKHdCs6Cd4RMfJPYnzgvI4BNrFUKsjkcu+WD4OO2A27Pq1n50cMchmcaXadJhGrOqH5YmHdOCj5NSHzJYrsW0HPlpuAx/ECMeIZYDh6RMqaFM2DXzdKX9NmmyqzJ3o/0lkk/N97gfVRLW5hA29yeAwaCViZNCP8iC9aO0q9fQojoa7NQnAtw=="
}
]
}
}
}
+1
View File
@@ -0,0 +1 @@
[{"Name":"MR20GA","ImageFileName":"LG_MR20GA.jpg","Buttons":[{"Name":null,"Button":23,"Preset":null,"Top":32.506480223994814,"Left":16.7759},{"Name":null,"Button":24,"Preset":null,"Top":39.65128022399482,"Left":17.6379},{"Name":null,"Button":12,"Preset":null,"Top":13.376880223994814,"Left":19.3621},{"Name":null,"Button":13,"Preset":null,"Top":13.376880223994814,"Left":41.7759},{"Name":null,"Button":22,"Preset":null,"Top":7.038780223994813,"Left":22.8103},{"Name":null,"Button":14,"Preset":null,"Top":13.376880223994814,"Left":64.6207},{"Name":null,"Button":15,"Preset":null,"Top":17.986380223994814,"Left":19.3621},{"Name":null,"Button":16,"Preset":null,"Top":17.986380223994814,"Left":41.7759},{"Name":null,"Button":17,"Preset":null,"Top":17.986380223994814,"Left":65.0517},{"Name":null,"Button":18,"Preset":null,"Top":22.365480223994815,"Left":19.3621},{"Name":null,"Button":19,"Preset":null,"Top":22.365480223994815,"Left":41.7759},{"Name":null,"Button":20,"Preset":null,"Top":22.365480223994815,"Left":65.0517},{"Name":null,"Button":21,"Preset":null,"Top":26.629280223994815,"Left":41.7759},{"Name":null,"Button":32,"Preset":null,"Top":26.744580223994816,"Left":64.6207},{"Name":null,"Button":25,"Preset":null,"Top":32.39128022399481,"Left":42.6379},{"Name":null,"Button":0,"Preset":null,"Top":44.49128022399481,"Left":12.465499999999999},{"Name":null,"Button":26,"Preset":null,"Top":44.49128022399481,"Left":72.3793},{"Name":null,"Button":29,"Preset":null,"Top":32.506480223994814,"Left":67.2069},{"Name":null,"Button":30,"Preset":null,"Top":39.42078022399482,"Left":67.6379},{"Name":null,"Button":4,"Preset":null,"Top":48.17888022399482,"Left":42.2069},{"Name":null,"Button":5,"Preset":null,"Top":61.89228022399482,"Left":43.069},{"Name":null,"Button":6,"Preset":null,"Top":55.093180223994814,"Left":17.2069},{"Name":null,"Button":7,"Preset":null,"Top":55.20848022399482,"Left":68.931},{"Name":null,"Button":2,"Preset":null,"Top":54.97798022399481,"Left":43.069},{"Name":null,"Button":1,"Preset":null,"Top":65.0036802239948,"Left":12.8966},{"Name":null,"Button":40,"Preset":null,"Top":64.7732802239948,"Left":72.8103},{"Name":null,"Button":42,"Preset":null,"Top":70.1894802239948,"Left":15.482800000000001},{"Name":null,"Button":41,"Preset":null,"Top":69.95898022399481,"Left":71.5172},{"Name":null,"Button":35,"Preset":null,"Top":79.6389802239948,"Left":44.3621},{"Name":null,"Button":31,"Preset":null,"Top":26.744580223994816,"Left":18.931},{"Name":null,"Button":36,"Preset":null,"Top":79.86948022399481,"Left":66.3448},{"Name":null,"Button":8,"Preset":null,"Top":75.2599802239948,"Left":15.0517},{"Name":null,"Button":9,"Preset":null,"Top":74.7989802239948,"Left":33.5862},{"Name":null,"Button":10,"Preset":null,"Top":75.14468022399481,"Left":52.9828},{"Name":null,"Button":11,"Preset":null,"Top":75.02948022399481,"Left":71.0862}]},{"Name":"MR23GN","ImageFileName":"LG_MR23GN.jpg","Buttons":[{"Name":null,"Button":22,"Preset":null,"Top":3.2806882676774514,"Left":30.569000000000003},{"Name":null,"Button":12,"Preset":null,"Top":8.671588267677452,"Left":16.3448},{"Name":null,"Button":13,"Preset":null,"Top":8.671588267677452,"Left":43.931},{"Name":null,"Button":14,"Preset":null,"Top":8.671588267677452,"Left":71.0862},{"Name":null,"Button":15,"Preset":null,"Top":13.581088267677451,"Left":16.3448},{"Name":null,"Button":16,"Preset":null,"Top":13.773588267677452,"Left":43.931},{"Name":null,"Button":17,"Preset":null,"Top":13.869888267677451,"Left":71.5172},{"Name":null,"Button":18,"Preset":null,"Top":18.77938826767745,"Left":16.3448},{"Name":null,"Button":19,"Preset":null,"Top":18.68318826767745,"Left":43.931},{"Name":null,"Button":20,"Preset":null,"Top":18.68318826767745,"Left":71.0862},{"Name":null,"Button":21,"Preset":null,"Top":23.88148826767745,"Left":43.931},{"Name":null,"Button":40,"Preset":null,"Top":23.78518826767745,"Left":16.3448},{"Name":null,"Button":32,"Preset":null,"Top":24.073988267677453,"Left":70.6552},{"Name":null,"Button":23,"Preset":null,"Top":29.272288267677453,"Left":15.482800000000001},{"Name":null,"Button":24,"Preset":null,"Top":37.069788267677446,"Left":15.913799999999998},{"Name":null,"Button":25,"Preset":null,"Top":33.02668826767745,"Left":43.069},{"Name":null,"Button":29,"Preset":null,"Top":29.368588267677453,"Left":71.0862},{"Name":null,"Button":24,"Preset":null,"Top":37.16608826767745,"Left":71.0862},{"Name":null,"Button":0,"Preset":null,"Top":42.171888267677446,"Left":15.482800000000001},{"Name":null,"Button":2,"Preset":null,"Top":51.894688267677445,"Left":43.069},{"Name":null,"Button":6,"Preset":null,"Top":51.50958826767745,"Left":15.0517},{"Name":null,"Button":7,"Preset":null,"Top":51.894688267677445,"Left":70.2241},{"Name":null,"Button":4,"Preset":null,"Top":45.15608826767745,"Left":42.6379},{"Name":null,"Button":5,"Preset":null,"Top":57.86308826767745,"Left":42.6379},{"Name":null,"Button":26,"Preset":null,"Top":60.94358826767745,"Left":71.0862},{"Name":null,"Button":1,"Preset":null,"Top":60.94358826767745,"Left":15.051Line truncated
@@ -0,0 +1 @@
[{"Name":"DefaultRC","ImageFileName":"Samsung_BN59-01315B.jpg","Buttons":[{"Name":null,"Button":26,"Preset":null,"Top":10.623333333333333,"Left":16.0},{"Name":null,"Button":13,"Preset":null,"Top":4.25,"Left":66.2604},{"Name":null,"Button":27,"Preset":null,"Top":10.623333333333333,"Left":42.2249},{"Name":null,"Button":28,"Preset":null,"Top":10.623333333333333,"Left":66.2604},{"Name":null,"Button":0,"Preset":null,"Top":4.373333333333333,"Left":16.0},{"Name":null,"Button":22,"Preset":null,"Top":35.456633333333336,"Left":16.4852},{"Name":null,"Button":23,"Preset":null,"Top":43.873333333333335,"Left":16.7811},{"Name":null,"Button":24,"Preset":null,"Top":34.790033333333334,"Left":41.3373},{"Name":null,"Button":12,"Preset":null,"Top":79.87333333333333,"Left":11.1598},{"Name":null,"Button":8,"Preset":null,"Top":69.70663333333333,"Left":14.414200000000001},{"Name":null,"Button":7,"Preset":null,"Top":63.20663333333333,"Left":41.3373},{"Name":null,"Button":10,"Preset":null,"Top":49.540033333333334,"Left":41.3373},{"Name":null,"Button":9,"Preset":null,"Top":69.70663333333333,"Left":66.1893},{"Name":null,"Button":14,"Preset":null,"Top":56.206633333333336,"Left":65.5976},{"Name":null,"Button":16,"Preset":null,"Top":79.87333333333333,"Left":31.278100000000002},{"Name":null,"Button":1,"Preset":null,"Top":56.623333333333335,"Left":41.3373},{"Name":null,"Button":3,"Preset":null,"Top":63.20663333333333,"Left":15.3018},{"Name":null,"Button":4,"Preset":null,"Top":63.20663333333333,"Left":67.3728},{"Name":null,"Button":2,"Preset":null,"Top":69.70663333333333,"Left":41.3373},{"Name":null,"Button":29,"Preset":null,"Top":15.841666666666667,"Left":16.0},{"Name":null,"Button":30,"Preset":null,"Top":15.841666666666667,"Left":40.929},{"Name":null,"Button":31,"Preset":null,"Top":15.841666666666667,"Left":66.2604},{"Name":null,"Button":32,"Preset":null,"Top":21.508366666666664,"Left":16.0},{"Name":null,"Button":33,"Preset":null,"Top":21.508366666666664,"Left":41.3373},{"Name":null,"Button":34,"Preset":null,"Top":21.508366666666664,"Left":66.2604},{"Name":null,"Button":25,"Preset":null,"Top":27.008366666666664,"Left":41.3373},{"Name":null,"Button":17,"Preset":null,"Top":75.09166666666667,"Left":12.526599999999998},{"Name":null,"Button":18,"Preset":null,"Top":75.09166666666667,"Left":30.573999999999998},{"Name":null,"Button":19,"Preset":null,"Top":75.09166666666667,"Left":50.1006},{"Name":null,"Button":20,"Preset":null,"Top":75.09166666666667,"Left":68.1479},{"Name":null,"Button":11,"Preset":null,"Top":42.041965520561135,"Left":40.7273}]},{"Name":"ServiceRC","ImageFileName":"Samsung_AA81-00243A.jpg","Buttons":[{"Name":null,"Button":16,"Preset":null,"Top":71.10894129883307,"Left":22.3828},{"Name":null,"Button":45,"Preset":null,"Top":35.282841298833084,"Left":15.2984},{"Name":null,"Button":0,"Preset":null,"Top":7.5743412988330805,"Left":20.748},{"Name":null,"Button":13,"Preset":null,"Top":7.79084129883308,"Left":67.8869},{"Name":null,"Button":15,"Preset":null,"Top":17.965041298833082,"Left":73.3365},{"Name":null,"Button":9,"Preset":null,"Top":35.39104129883308,"Left":74.9714},{"Name":null,"Button":7,"Preset":null,"Top":27.05684129883308,"Left":44.4537},{"Name":null,"Button":1,"Preset":null,"Top":19.480341298833082,"Left":44.9986},{"Name":null,"Button":3,"Preset":null,"Top":26.94864129883308,"Left":25.6526},{"Name":null,"Button":4,"Preset":null,"Top":26.94864129883308,"Left":64.3447},{"Name":null,"Button":2,"Preset":null,"Top":34.41694129883308,"Left":45.5436},{"Name":null,"Button":12,"Preset":null,"Top":17.423841298833082,"Left":15.8433},{"Name":null,"Button":26,"Preset":null,"Top":51.30174129883308,"Left":22.3828},{"Name":null,"Button":27,"Preset":null,"Top":50.97704129883308,"Left":46.361},{"Name":null,"Button":28,"Preset":null,"Top":51.08524129883308,"Left":68.9768},{"Name":null,"Button":29,"Preset":null,"Top":57.90414129883308,"Left":22.6553},{"Name":null,"Button":30,"Preset":null,"Top":57.68764129883308,"Left":45.8161},{"Name":null,"Button":31,"Preset":null,"Top":57.79594129883308,"Left":68.7044},{"Name":null,"Button":32,"Preset":null,"Top":64.61474129883307,"Left":22.1104},{"Name":null,"Button":33,"Preset":null,"Top":64.50654129883307,"Left":45.2711},{"Name":null,"Button":34,"Preset":null,"Top":64.29004129883307,"Left":68.9768},{"Name":null,"Button":25,"Preset":null,"Top":70.89244129883308,"Left":45.8161}]}]
Loaded 100 of 313 files, more files were not shown because too many files have changed in this diff. Show more