diff --git a/.idea/runConfigurations/Run.xml b/.idea/runConfigurations/Run.xml
index 2e415db..beddb34 100644
--- a/.idea/runConfigurations/Run.xml
+++ b/.idea/runConfigurations/Run.xml
@@ -8,7 +8,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Run_Standalone.xml b/.idea/runConfigurations/Run_Standalone.xml
new file mode 100644
index 0000000..71c7d3c
--- /dev/null
+++ b/.idea/runConfigurations/Run_Standalone.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/HardwareMonitor/HardwareMonitor.sln.DotSettings.user b/HardwareMonitor/HardwareMonitor.sln.DotSettings.user
index d8ae40b..43446f3 100644
--- a/HardwareMonitor/HardwareMonitor.sln.DotSettings.user
+++ b/HardwareMonitor/HardwareMonitor.sln.DotSettings.user
@@ -1,8 +1,10 @@
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
@@ -10,6 +12,7 @@
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
\ No newline at end of file
diff --git a/HardwareMonitor/HardwareMonitor/Monitor/MonitorPacketCommand.cs b/HardwareMonitor/HardwareMonitor/Monitor/MonitorPacketCommand.cs
new file mode 100644
index 0000000..b3f16c9
--- /dev/null
+++ b/HardwareMonitor/HardwareMonitor/Monitor/MonitorPacketCommand.cs
@@ -0,0 +1,9 @@
+namespace HardwareMonitor.Monitor;
+
+public enum MonitorPacketCommand : short
+{
+ Data = 0,
+ RefreshPresentMonApps = 1,
+ SelectPresentMonApp = 2,
+ PresentMonApps = 3,
+}
\ No newline at end of file
diff --git a/HardwareMonitor/HardwareMonitor/Monitor/MonitorPoller.cs b/HardwareMonitor/HardwareMonitor/Monitor/MonitorPoller.cs
index d060890..5459f9e 100644
--- a/HardwareMonitor/HardwareMonitor/Monitor/MonitorPoller.cs
+++ b/HardwareMonitor/HardwareMonitor/Monitor/MonitorPoller.cs
@@ -35,8 +35,10 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
_computer.Open();
_computer.Accept(new UpdateVisitor());
- _presentMonPoller.Start();
+ _presentMonPoller.Start(stoppingToken);
_socketHost.StartServer();
+ _socketHost.onClientData += OnClientData;
+ _socketHost.onClientConnected += OnClientConnected;
var sharedMemoryData = QueryHardwareData();
@@ -46,6 +48,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
using var writer = new BinaryWriter(memoryStream);
var accumulator = 0;
+ writer.Write((short)MonitorPacketCommand.Data);
writer.Write(sharedMemoryData.Hardwares.Count);
writer.Write(sharedMemoryData.Sensors.Count);
@@ -97,6 +100,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
GC.Collect();
accumulator = 0;
+ SendPresentMonAppsToClients();
}
accumulator += 500;
@@ -107,6 +111,60 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
hostApplicationLifetime.StopApplication();
}
+ private void OnClientConnected()
+ {
+ SendPresentMonAppsToClients();
+ }
+
+ private void OnClientData(byte[] data)
+ {
+ var cmd = (MonitorPacketCommand) BitConverter.ToInt16(data, 0);
+ logger.LogInformation("Received command from client: {Command}", cmd);
+ switch (cmd)
+ {
+ case MonitorPacketCommand.RefreshPresentMonApps:
+ SendPresentMonAppsToClients();
+ break;
+ case MonitorPacketCommand.SelectPresentMonApp:
+ SelectPresentMonApp(data);
+ break;
+
+ // server -> client cases
+ case MonitorPacketCommand.Data:
+ case MonitorPacketCommand.PresentMonApps:
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ private void SelectPresentMonApp(byte[] data)
+ {
+ // start at 2 because the first 2 were the command
+ var size = BitConverter.ToInt16(data, 2);
+ var appName = Encoding.UTF8.GetString(data, 4, size);
+ _presentMonPoller.SetSelectedApp(appName);
+ }
+
+ private void SendPresentMonAppsToClients()
+ {
+ using var memoryStream = new MemoryStream();
+ using var writer = new BinaryWriter(memoryStream);
+
+ writer.Write((short)MonitorPacketCommand.PresentMonApps);
+ //logger.LogInformation("Sending presentmon apps to clients {Count}", _presentMonPoller.CurrentApps.Count);
+ writer.Write((short)_presentMonPoller.CurrentApps.Count);
+ foreach (var app in _presentMonPoller.CurrentApps)
+ {
+ writer.Write(GetBytes(app, SharedMemoryConsts.NameSize));
+ }
+
+ if (_socketHost.HasConnections())
+ {
+ _socketHost.SendToAll(memoryStream.ToArray());
+ }
+ }
+
private SharedMemoryData QueryHardwareData()
{
var hardwareList = new List();
@@ -155,6 +213,7 @@ private void Stop()
_computer.Close();
_presentMonPoller.Stop();
_socketHost.Close();
+ _socketHost.onClientData -= OnClientData;
}
private static SharedMemoryHardware MapHardware(IHardware hardware) => new()
diff --git a/HardwareMonitor/HardwareMonitor/PresentMon/PresentMonPoller.cs b/HardwareMonitor/HardwareMonitor/PresentMon/PresentMonPoller.cs
index 842dffe..f2ba92e 100644
--- a/HardwareMonitor/HardwareMonitor/PresentMon/PresentMonPoller.cs
+++ b/HardwareMonitor/HardwareMonitor/PresentMon/PresentMonPoller.cs
@@ -7,45 +7,58 @@ namespace HardwareMonitor.PresentMon;
public class PresentMonPoller(ILogger logger)
{
+ private const string NO_SELECTED_APP = "NONE";
+
private IHardware _hardware = new PresentMonHardware();
public PresentMonSensor Displayed { get; private set; }
public PresentMonSensor Presented { get; private set; }
public PresentMonSensor Frametime { get; private set; }
+ public HashSet CurrentApps { get; private set; }
private Process _process;
private CultureInfo _cultureInfo = (CultureInfo)CultureInfo.CurrentCulture.Clone();
- public async void Start()
+ private string _currentSelectedApp = NO_SELECTED_APP;
+
+
+ public async void Start(CancellationToken stoppingToken)
{
_cultureInfo.NumberFormat.NumberDecimalSeparator = ".";
Displayed = new PresentMonSensor(_hardware, "displayed", 0, "Displayed Frames");
Presented = new PresentMonSensor(_hardware, "presented", 1, "Presented Frames");
Frametime = new PresentMonSensor(_hardware, "frametime", 2, "Frametime");
+ CurrentApps = [];
- using var reader = new StreamReader(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "presentmon", "ignored-processes.txt"));
- var text = await reader.ReadToEndAsync();
- var processes = text
+ using var reader = new StreamReader(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "presentmon",
+ "ignored-processes.txt"));
+ var text = (await reader.ReadToEndAsync())
.Split("\n", StringSplitOptions.RemoveEmptyEntries)
.Select(x => $"--exclude {x.Trim()}");
+ var filteredApps = string.Join(" ", text);
+ await TerminateCurrentPresentMon();
var processStartInfo = new ProcessStartInfo
{
CreateNoWindow = true,
RedirectStandardOutput = true,
+ RedirectStandardError = true,
UseShellExecute = false,
FileName = "presentmon\\presentmon.exe",
- Arguments = $"--stop_existing_session --no_console_stats --output_stdout {string.Join(' ', processes)}"
+ Arguments = $"--stop_existing_session --no_console_stats --output_stdout --session_name HardwareMonitor {filteredApps}",
};
+ logger.LogInformation("Starting PresentMon process with {Arguments}", processStartInfo.Arguments);
+
_process = new Process();
_process.StartInfo = processStartInfo;
-
_process.OutputDataReceived += (sender, args) => ParseData(args.Data);
- _process.Exited += (sender, args) => Start();
-
+ _process.ErrorDataReceived += (sender, args) => logger.LogError(args.Data);
+
_process.Start();
_process.BeginOutputReadLine();
+ _process.BeginErrorReadLine();
+ ClearCurrentAppsAsync(stoppingToken);
await _process.WaitForExitAsync();
}
@@ -60,6 +73,13 @@ private void ParseData(string? argsData)
if (argsData != null)
{
parts = argsData.Split(",");
+ CurrentApps.Add(parts[0]);
+
+ if (_currentSelectedApp != NO_SELECTED_APP && _currentSelectedApp != parts[0])
+ {
+ return;
+ }
+
if (float.TryParse(parts[9], NumberStyles.Any, _cultureInfo, out var frametime))
{
Frametime.Value = frametime;
@@ -76,4 +96,39 @@ private void ParseData(string? argsData)
}
}
}
+
+ public void SetSelectedApp(string appName)
+ {
+ if (appName == "Auto") {
+ _currentSelectedApp = NO_SELECTED_APP;
+ return;
+ }
+ _currentSelectedApp = appName;
+ }
+ private async Task TerminateCurrentPresentMon()
+ {
+ var processStartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ FileName = "presentmon\\presentmon.exe",
+ Arguments = $"--terminate_existing_session --no_console_stats --output_stdout --session_name HardwareMonitor",
+ };
+ logger.LogInformation("Starting PresentMon process with {Arguments}", processStartInfo.Arguments);
+
+ var process = new Process();
+ process.StartInfo = processStartInfo;
+ process.Start();
+ await process.WaitForExitAsync();
+ }
+
+ private async Task ClearCurrentAppsAsync(CancellationToken cancellationToken)
+ {
+ if (cancellationToken.IsCancellationRequested) return;
+ await Task.Delay(10_000, cancellationToken);
+ CurrentApps.Clear();
+ ClearCurrentAppsAsync(cancellationToken);
+ }
}
\ No newline at end of file
diff --git a/HardwareMonitor/HardwareMonitor/Program.cs b/HardwareMonitor/HardwareMonitor/Program.cs
index 76481e4..3906db8 100644
--- a/HardwareMonitor/HardwareMonitor/Program.cs
+++ b/HardwareMonitor/HardwareMonitor/Program.cs
@@ -16,7 +16,7 @@
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "LogFiles",
$"{DateTime.Now.Year}-{DateTime.Now.Month}-{DateTime.Now.Day}", "Log.txt"),
rollingInterval: RollingInterval.Infinite,
- outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}")
+ outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level}] {Message}{NewLine}{Exception}")
.WriteTo.Console()
);
diff --git a/HardwareMonitor/HardwareMonitor/Sockets/SocketHost.cs b/HardwareMonitor/HardwareMonitor/Sockets/SocketHost.cs
index 49b6d25..a62019d 100644
--- a/HardwareMonitor/HardwareMonitor/Sockets/SocketHost.cs
+++ b/HardwareMonitor/HardwareMonitor/Sockets/SocketHost.cs
@@ -8,12 +8,16 @@ public class SocketHost(ILogger logger)
{
private Socket _listener;
private List _clients = new();
-
+ private byte[] _receiveBuffer = new byte[2048];
+
+ public Action onClientData;
+ public Action onClientConnected;
+
public async void StartServer()
{
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, 31337);
logger.LogInformation("Listening for connections on {LocalEndPoint}", localEndPoint);
-
+
_listener = new Socket(localEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_listener.Bind(localEndPoint);
_listener.Listen();
@@ -25,9 +29,24 @@ private void OnConnection(IAsyncResult asyncResult)
{
var server = (Socket)asyncResult.AsyncState!;
var client = server.EndAccept(asyncResult);
-
+ client.BeginReceive(_receiveBuffer, 0, _receiveBuffer.Length, SocketFlags.None, OnDataReceived, client);
+
_clients.Add(client);
server.BeginAccept(OnConnection, server);
+ onClientConnected?.Invoke();
+ }
+
+ private void OnDataReceived(IAsyncResult asyncResult)
+ {
+ var client = (Socket)asyncResult.AsyncState!;
+ int received = client.EndReceive(asyncResult);
+
+ if (received > 0)
+ {
+ onClientData?.Invoke(_receiveBuffer);
+ }
+
+ client.BeginReceive(_receiveBuffer, 0, _receiveBuffer.Length, SocketFlags.None, OnDataReceived, client);
}
public void Close()
@@ -35,18 +54,21 @@ public void Close()
_clients.ForEach(it => it.Close());
_listener.Close();
}
-
+
public bool HasConnections() => _clients.Count > 0;
public void SendToAll(byte[] memoryStream)
{
+ var listWithSize = memoryStream.ToList();
+ listWithSize.InsertRange(2, BitConverter.GetBytes(listWithSize.Count - 2));
for (var i = 0; i < _clients.Count; i++)
{
if (_clients[i].IsConnected())
{
- _clients[i].SendAsync(memoryStream, SocketFlags.None);
+ _clients[i].SendAsync(listWithSize.ToArray(), SocketFlags.None);
}
}
+ listWithSize.Clear();
}
}
@@ -58,6 +80,9 @@ public static bool IsConnected(this Socket socket)
{
return !(socket.Poll(1, SelectMode.SelectRead) && socket.Available == 0);
}
- catch (SocketException) { return false; }
+ catch (SocketException)
+ {
+ return false;
+ }
}
}
\ No newline at end of file
diff --git a/core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/HardwareMonitorData.kt b/core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/HardwareMonitorData.kt
index 6d8d555..589b62a 100644
--- a/core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/HardwareMonitorData.kt
+++ b/core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/HardwareMonitorData.kt
@@ -6,7 +6,8 @@ import kotlinx.serialization.Serializable
data class HardwareMonitorData(
val LastPollTime: Long,
val Hardwares: List,
- val Sensors: List
+ val Sensors: List,
+ val PresentMonApps: List,
) {
@Serializable
data class Hardware(
diff --git a/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Typography.kt b/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Typography.kt
index 80ebc16..f26e28c 100644
--- a/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Typography.kt
+++ b/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Typography.kt
@@ -119,6 +119,18 @@ class Typography {
* lineHeight = 0.sp,
* fontWeight = W400,
*/
+ val labelSMedium: TextStyle
+ @Composable get() = defaultTextStyle.copy(
+ fontSize = 12.sp,
+ lineHeight = 0.sp,
+ fontFamily = fontFamilyMedium,
+ )
+
+ /**
+ * fontSize = 12.sp,
+ * lineHeight = 0.sp,
+ * fontWeight = W600,
+ */
val labelSSemiBold: TextStyle
@Composable get() = defaultTextStyle.copy(
fontSize = 12.sp,
@@ -148,7 +160,7 @@ class Typography {
// Font(resource = "font/inter_black.ttf", weight = FontWeight.Black),
private val fontFamilyThin = FontFamily(
- Font(resource = "font/inter_thin.ttf", weight = FontWeight.Normal),
+ Font(resource = "font/inter_thin.ttf", weight = FontWeight.SemiBold),
)
private val fontFamilyNormal = FontFamily(
diff --git a/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorProcessManager.kt b/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorProcessManager.kt
index 2772235..3d5bb6e 100644
--- a/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorProcessManager.kt
+++ b/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorProcessManager.kt
@@ -10,6 +10,12 @@ import java.util.*
object HardwareMonitorProcessManager {
private var process: Process? = null
+ fun checkRuntime() {
+ ProcessBuilder().apply {
+ command("dotnet --list-runtimes")
+ }.start()
+ }
+
fun start() {
val currentDir = Path.of("").toAbsolutePath().toString()
val file = if (isDev()) {
diff --git a/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorReader.kt b/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorReader.kt
index 24c3299..46295b8 100644
--- a/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorReader.kt
+++ b/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorReader.kt
@@ -4,13 +4,15 @@ import app.cleanmeter.core.common.hardwaremonitor.HardwareMonitorData
import app.cleanmeter.core.os.util.getByteBuffer
import app.cleanmeter.core.os.util.readString
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
-import java.io.InputStream
-import java.net.InetSocketAddress
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.mapNotNull
import java.net.Socket
-import java.net.SocketException
import java.nio.ByteBuffer
private const val HARDWARE_SIZE = 260
@@ -18,57 +20,52 @@ private const val SENSOR_SIZE = 392
private const val NAME_SIZE = 128
private const val IDENTIFIER_SIZE = 128
private const val HEADER_SIZE = 8
+private const val LENGTH_SIZE = 2
-object HardwareMonitorReader {
-
- val currentData = flow {
- var socket = Socket()
+enum class Command(val value: Short) {
+ Data(0),
+ RefreshPresentMonApps(1),
+ SelectPresentMonApp(2),
+ PresentMonApps(3);
- while (true) {
+ companion object {
+ fun fromValue(value: Short) = entries.find { it.value == value } ?: Data
+ }
+}
- // try open a connection with HardwareMonitor
- if (!socket.isConnected) {
- try {
- println("Trying to connect")
- socket = Socket()
- socket.connect(InetSocketAddress("0.0.0.0", 31337))
- println("Connected ${socket.isConnected}")
- } catch (ex: Exception) {
- println("Couldn't connect ${ex.message}")
- if (ex !is SocketException) {
- ex.printStackTrace()
- }
- } finally {
- delay(500)
- continue
- }
- }
+object HardwareMonitorReader {
- val inputStream = socket.inputStream
- while (socket.isConnected) {
- try {
+ private var _currentData: HardwareMonitorData = HardwareMonitorData(0L, emptyList(), emptyList(), emptyList())
+ val currentData: Flow = SocketClient
+ .packetFlow
+ .mapNotNull { packet ->
+ when (packet) {
+ is Packet.Data -> {
// read first 8 bytes to get the amount of hardware and sensors
- val (hardware, sensor) = readHardwareAndSensorCount(inputStream)
+ val (hardware, sensor) = readHardwareAndSensorCount(packet.data)
+ if (hardware + sensor <= 0) return@mapNotNull null
- // if both are 0, bail
- if (hardware + sensor == 0) continue
-
- // we know the length in bytes of hardware and sensor, so we know the length of the packet
- val buffer = getByteBuffer(inputStream, hardware * HARDWARE_SIZE + sensor * SENSOR_SIZE)
+ val buffer = getByteBuffer(packet.data, hardware * HARDWARE_SIZE + sensor * SENSOR_SIZE, HEADER_SIZE)
val hardwares = readHardware(buffer, hardware)
val sensors = readSensor(buffer, sensor)
- emit(HardwareMonitorData(0L, hardwares, sensors))
- } catch (e: SocketException) {
- socket.close()
- socket = Socket()
- e.printStackTrace()
+ _currentData = _currentData.copy(Hardwares = hardwares, Sensors = sensors)
+ _currentData
+ }
+
+ is Packet.PresentMonApps -> {
+ val appsCount = getByteBuffer(packet.data, LENGTH_SIZE, 0).short
+ val buffer = getByteBuffer(packet.data, appsCount.toInt() * NAME_SIZE, LENGTH_SIZE)
+ val apps = listOf("Auto") + readPresentMonApps(buffer, appsCount)
+ _currentData = _currentData.copy(PresentMonApps = apps)
+ _currentData
}
+
+ is Packet.SelectPresentMonApp -> null
}
}
- }.flowOn(Dispatchers.IO)
- private fun readHardwareAndSensorCount(input: InputStream): Pair {
- val buffer = getByteBuffer(input, HEADER_SIZE)
+ private fun readHardwareAndSensorCount(input: ByteArray): Pair {
+ val buffer = getByteBuffer(input, HEADER_SIZE, 0)
return buffer.int to buffer.int
}
@@ -99,4 +96,12 @@ object HardwareMonitorReader {
}
}
}
+
+ private fun readPresentMonApps(buffer: ByteBuffer, count: Short): List {
+ return buildList {
+ for (i in 0 until count) {
+ add(buffer.readString(NAME_SIZE))
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/SocketClient.kt b/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/SocketClient.kt
new file mode 100644
index 0000000..975c90b
--- /dev/null
+++ b/core/native/src/main/kotlin/app/cleanmeter/core/os/hardwaremonitor/SocketClient.kt
@@ -0,0 +1,103 @@
+package app.cleanmeter.core.os.hardwaremonitor
+
+import app.cleanmeter.core.os.util.getByteBuffer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+import java.io.InputStream
+import java.net.InetSocketAddress
+import java.net.Socket
+import java.net.SocketException
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+
+private const val COMMAND_SIZE = 2
+private const val LENGTH_SIZE = 4
+
+sealed class Packet {
+ data class Data(val data: ByteArray) : Packet()
+ data class PresentMonApps(val data: ByteArray) : Packet()
+ data class SelectPresentMonApp(val name: String) : Packet()
+}
+
+object SocketClient {
+
+ private var socket = Socket()
+
+ private val packetChannel = Channel(Channel.CONFLATED)
+ val packetFlow: Flow = packetChannel.receiveAsFlow()
+
+ init {
+ connect()
+ }
+
+ private fun connect() = CoroutineScope(Dispatchers.IO).launch {
+ while (true) {
+ // try open a connection with HardwareMonitor
+ if (!socket.isConnected) {
+ try {
+ println("Trying to connect")
+ socket = Socket()
+ socket.connect(InetSocketAddress("0.0.0.0", 31337))
+ println("Connected ${socket.isConnected}")
+ } catch (ex: Exception) {
+ println("Couldn't connect ${ex.message}")
+// if (ex !is SocketException) {
+ ex.printStackTrace()
+// }
+ } finally {
+ delay(500)
+ continue
+ }
+ }
+
+ val inputStream = socket.inputStream
+ while(socket.isConnected) {
+ try {
+ val command = getCommand(inputStream)
+ val size = getSize(inputStream)
+ when (command) {
+ Command.Data -> packetChannel.trySend(Packet.Data(inputStream.readNBytes(size)))
+ Command.PresentMonApps -> packetChannel.trySend(Packet.PresentMonApps(inputStream.readNBytes(size)))
+ Command.RefreshPresentMonApps -> Unit
+ Command.SelectPresentMonApp -> Unit
+ }
+ } catch (e: SocketException) {
+ socket.close()
+ socket = Socket()
+ e.printStackTrace()
+ }
+ }
+ }
+ }
+
+ private fun getCommand(inputStream: InputStream): Command {
+ val buffer = getByteBuffer(inputStream, COMMAND_SIZE)
+ return Command.fromValue(buffer.short)
+ }
+
+ private fun getSize(inputStream: InputStream): Int {
+ val buffer = getByteBuffer(inputStream, LENGTH_SIZE)
+ return buffer.int
+ }
+
+ fun sendPacket(selectPresentMonApp: Packet.SelectPresentMonApp) {
+ if (socket.isConnected) {
+ val nameBytes = selectPresentMonApp.name.toByteArray()
+ val buffer = ByteBuffer.allocate(2 + 2 + nameBytes.count()).order(ByteOrder.LITTLE_ENDIAN).apply {
+ putShort(Command.SelectPresentMonApp.value)
+ putShort(nameBytes.size.toShort())
+ put(nameBytes)
+ }.array()
+ println("Sending ${buffer.count()} bytes")
+ socket.outputStream.apply {
+ write(buffer)
+ flush()
+ }
+ }
+ }
+}
diff --git a/core/native/src/main/kotlin/app/cleanmeter/core/os/util/MemoryUtils.kt b/core/native/src/main/kotlin/app/cleanmeter/core/os/util/MemoryUtils.kt
index 9b50402..ddf5aa7 100644
--- a/core/native/src/main/kotlin/app/cleanmeter/core/os/util/MemoryUtils.kt
+++ b/core/native/src/main/kotlin/app/cleanmeter/core/os/util/MemoryUtils.kt
@@ -8,9 +8,15 @@ import java.nio.charset.Charset
import java.util.Locale
internal fun getByteBuffer(input: InputStream, length: Int): ByteBuffer {
+ if (length <= 0) return ByteBuffer.allocate(0).order(ByteOrder.LITTLE_ENDIAN)
return ByteBuffer.wrap(input.readNBytes(length)).order(ByteOrder.LITTLE_ENDIAN)
}
+internal fun getByteBuffer(input: ByteArray, length: Int, offset: Int): ByteBuffer {
+ if (length <= 0) return ByteBuffer.allocate(0)
+ return ByteBuffer.wrap(input).slice(offset, length).order(ByteOrder.LITTLE_ENDIAN)
+}
+
internal fun getByteBuffer(pointer: Pointer, size: Int, offset: Int = 0): ByteBuffer {
val buffer = ByteBuffer.allocateDirect(size)
buffer.put(pointer.getByteArray(0, size))
diff --git a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/CheckboxWithLabel.kt b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/CheckboxWithLabel.kt
index 613445b..5c0cddc 100644
--- a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/CheckboxWithLabel.kt
+++ b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/CheckboxWithLabel.kt
@@ -33,7 +33,8 @@ fun CheckboxWithLabel(
colors = CheckboxDefaults.colors(
checkedColor = LocalColorScheme.current.background.brand,
uncheckedColor = LocalColorScheme.current.background.surfaceSunken,
- checkmarkColor = LocalColorScheme.current.background.surfaceRaised
+ checkmarkColor = LocalColorScheme.current.background.surfaceRaised,
+ disabledColor = LocalColorScheme.current.background.surfaceSunkenSubtle,
),
modifier = Modifier.size(24.dp)
)
diff --git a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/dropdown/DropdownMenu.kt b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/dropdown/DropdownMenu.kt
index 2d4fe3e..0348243 100644
--- a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/dropdown/DropdownMenu.kt
+++ b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/components/dropdown/DropdownMenu.kt
@@ -3,10 +3,12 @@ package app.cleanmeter.target.desktop.ui.components.dropdown
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
@@ -15,6 +17,7 @@ import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.rounded.ChevronRight
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -32,9 +35,12 @@ import app.cleanmeter.core.designsystem.LocalTypography
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun DropdownMenu(
+ label: String? = null,
+ disclaimer: String? = null,
options: List,
selectedIndex: Int,
- onValueChanged: (Int) -> Unit
+ onValueChanged: (Int) -> Unit,
+ modifier: Modifier = Modifier,
) {
var expanded by remember { mutableStateOf(false) }
var selectedOption by remember { mutableStateOf(options[selectedIndex]) }
@@ -42,33 +48,69 @@ fun DropdownMenu(
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded },
- modifier = Modifier.fillMaxWidth(),
+ modifier = modifier.fillMaxWidth(),
) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier
- .fillMaxWidth()
- .border(1.dp, LocalColorScheme.current.border.bolder, RoundedCornerShape(8.dp))
- .background(LocalColorScheme.current.background.surfaceRaised).padding(12.dp)
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
) {
- Text(
- text = selectedOption,
- color = LocalColorScheme.current.text.heading,
- style = LocalTypography.current.labelL,
- modifier = Modifier.align(Alignment.CenterVertically)
- )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .fillMaxWidth()
+ .border(1.dp, LocalColorScheme.current.border.bolder, RoundedCornerShape(8.dp))
+ .background(LocalColorScheme.current.background.surfaceRaised).padding(12.dp)
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ if (label != null) {
+ Text(
+ text = label,
+ color = LocalColorScheme.current.text.paragraph1,
+ style = LocalTypography.current.labelL,
+ modifier = Modifier.align(Alignment.CenterVertically)
+ )
+ }
- IconButton(onClick = { }, modifier = Modifier.size(20.dp).clearAndSetSemantics { }) {
- Icon(
- imageVector = Icons.Rounded.ChevronRight,
- contentDescription = "Trailing icon for exposed dropdown menu",
- tint = LocalColorScheme.current.icon.bolderActive,
- modifier = Modifier.rotate(
- if (expanded) 270f
- else 90f
+ Text(
+ text = selectedOption,
+ color = LocalColorScheme.current.text.heading,
+ style = LocalTypography.current.labelLMedium,
+ modifier = Modifier.align(Alignment.CenterVertically)
+ )
+ }
+
+ IconButton(onClick = { }, modifier = Modifier.size(20.dp).clearAndSetSemantics { }) {
+ Icon(
+ imageVector = Icons.Rounded.ChevronRight,
+ contentDescription = "Trailing icon for exposed dropdown menu",
+ tint = LocalColorScheme.current.icon.bolderActive,
+ modifier = Modifier.rotate(
+ if (expanded) 270f
+ else 90f
+ )
+ )
+ }
+ }
+
+ if (disclaimer != null) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Info,
+ contentDescription = "Trailing icon for exposed dropdown menu",
+ tint = LocalColorScheme.current.icon.bolderActive,
+ modifier = Modifier.size(16.dp)
)
- )
+ Text(
+ text = disclaimer,
+ color = LocalColorScheme.current.text.disabled,
+ style = LocalTypography.current.labelSMedium,
+ modifier = Modifier.wrapContentHeight(align = Alignment.CenterVertically)
+ )
+ }
}
}
diff --git a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/Settings.kt b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/Settings.kt
index 22fbe43..042d3c2 100644
--- a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/Settings.kt
+++ b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/Settings.kt
@@ -113,10 +113,14 @@ private fun TabContent(
onDisplaySelect = {
viewModel.onEvent(SettingsEvent.DisplaySelect(it))
},
+ onFpsApplicationSelect = {
+ viewModel.onEvent(SettingsEvent.FpsApplicationSelect(it))
+ },
getCpuSensorReadings = { settingsState.hardwareData?.cpuReadings() ?: emptyList() },
getGpuSensorReadings = { settingsState.hardwareData?.gpuReadings() ?: emptyList() },
getNetworkSensorReadings = { settingsState.hardwareData?.networkReadings() ?: emptyList() },
getHardwareSensors = { settingsState.hardwareData?.Hardwares ?: emptyList() },
+ getPresentMonApps = { settingsState.hardwareData?.PresentMonApps ?: emptyList() },
)
1 -> StyleUi(
diff --git a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/SettingsViewModel.kt b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/SettingsViewModel.kt
index 0bc2f35..a733bd4 100644
--- a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/SettingsViewModel.kt
+++ b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/SettingsViewModel.kt
@@ -3,6 +3,8 @@ package app.cleanmeter.target.desktop.ui.settings
import androidx.compose.ui.unit.IntOffset
import androidx.lifecycle.ViewModel
import app.cleanmeter.core.common.hardwaremonitor.HardwareMonitorData
+import app.cleanmeter.core.os.hardwaremonitor.Packet
+import app.cleanmeter.core.os.hardwaremonitor.SocketClient
import app.cleanmeter.target.desktop.data.ObserveHardwareReadings
import app.cleanmeter.target.desktop.data.OverlaySettingsRepository
import app.cleanmeter.target.desktop.model.OverlaySettings
@@ -31,6 +33,7 @@ sealed class SettingsEvent {
data class OverlayOpacityChange(val opacity: Float) : SettingsEvent()
data class OverlayGraphChange(val progressType: OverlaySettings.ProgressType) : SettingsEvent()
data class DarkThemeToggle(val isEnabled: Boolean) : SettingsEvent()
+ data class FpsApplicationSelect(val applicationName: String) : SettingsEvent()
}
class SettingsViewModel : ViewModel() {
@@ -75,9 +78,14 @@ class SettingsViewModel : ViewModel() {
is SettingsEvent.OverlayOpacityChange -> onOverlayOpacityChange(event.opacity, this)
is SettingsEvent.OverlayGraphChange -> onOverlayGraphChange(event.progressType, this)
is SettingsEvent.DarkThemeToggle -> onDarkModeToggle(event.isEnabled, this)
+ is SettingsEvent.FpsApplicationSelect -> onFpsApplicationSelect(event.applicationName, this)
}
}
+ private fun onFpsApplicationSelect(applicationName: String, settingsState: SettingsState) {
+ SocketClient.sendPacket(Packet.SelectPresentMonApp(applicationName))
+ }
+
private fun onDarkModeToggle(enabled: Boolean, settingsState: SettingsState) {
with(settingsState) {
val newSettings = overlaySettings?.copy(isDarkTheme = enabled)
diff --git a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/AppSettingsUi.kt b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/AppSettingsUi.kt
index a729064..efdfcee 100644
--- a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/AppSettingsUi.kt
+++ b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/AppSettingsUi.kt
@@ -18,6 +18,7 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AdminPanelSettings
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -110,33 +111,34 @@ fun AppSettingsUi(
private fun startWithWindowsCheckbox() {
var state by remember { mutableStateOf(WinRegistry.isAppRegisteredToStartWithWindows()) }
- CheckboxWithLabel(
- label = "Start with Windows",
- checked = state,
- onCheckedChange = { value ->
- state = value
- if (value) {
- WinRegistry.registerAppToStartWithWindows()
- } else {
- WinRegistry.removeAppFromStartWithWindows()
- }
+ LaunchedEffect(Unit) {
+ if (state) {
+ WinRegistry.removeAppFromStartWithWindows()
}
- ) {
- TooltipArea(
- delayMillis = 0,
- tooltip = {
- Text(
- text = "Admin rights needed",
- style = LocalTypography.current.labelM,
- color = LocalColorScheme.current.text.heading,
- )
- }) {
- Icon(
- imageVector = Icons.Filled.AdminPanelSettings,
- contentDescription = null,
- tint = LocalColorScheme.current.icon.bolderActive
+ }
+
+ TooltipArea(
+ delayMillis = 0,
+ tooltip = {
+ Text(
+ text = "Temporarily disabled.",
+ style = LocalTypography.current.labelM,
+ color = LocalColorScheme.current.text.heading,
)
- }
+ }) {
+ CheckboxWithLabel(
+ label = "Start with Windows",
+ checked = state,
+ enabled = false,
+ onCheckedChange = { value ->
+ state = value
+ if (value) {
+ WinRegistry.registerAppToStartWithWindows()
+ } else {
+ WinRegistry.removeAppFromStartWithWindows()
+ }
+ }
+ )
}
}
diff --git a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/OverlaySettingsUi.kt b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/OverlaySettingsUi.kt
index cf163b1..30d74d0 100644
--- a/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/OverlaySettingsUi.kt
+++ b/target/desktop/src/main/kotlin/app/cleanmeter/target/desktop/ui/settings/tabs/OverlaySettingsUi.kt
@@ -17,12 +17,14 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.cleanmeter.core.common.hardwaremonitor.HardwareMonitorData
import app.cleanmeter.core.designsystem.LocalColorScheme
+import app.cleanmeter.core.designsystem.LocalTypography
import app.cleanmeter.target.desktop.model.OverlaySettings
import app.cleanmeter.target.desktop.ui.components.section.CheckboxSection
import app.cleanmeter.target.desktop.ui.components.CheckboxWithLabel
import app.cleanmeter.target.desktop.ui.components.section.CustomBodyCheckboxSection
import app.cleanmeter.target.desktop.ui.components.section.DropdownSection
import app.cleanmeter.target.desktop.ui.components.KeyboardShortcutInfoLabel
+import app.cleanmeter.target.desktop.ui.components.dropdown.DropdownMenu
import app.cleanmeter.target.desktop.ui.components.dropdown.SensorReadingDropdownMenu
import app.cleanmeter.target.desktop.ui.settings.CheckboxSectionOption
import app.cleanmeter.target.desktop.ui.settings.SectionType
@@ -39,10 +41,12 @@ fun OverlaySettingsUi(
onSectionSwitchToggle: (SectionType, Boolean) -> Unit,
onCustomSensorSelect: (SensorType, String) -> Unit,
onDisplaySelect: (Int) -> Unit,
+ onFpsApplicationSelect: (String) -> Unit,
getCpuSensorReadings: () -> List,
getGpuSensorReadings: () -> List,
getNetworkSensorReadings: () -> List,
getHardwareSensors: () -> List,
+ getPresentMonApps: () -> List,
) = Column(
modifier = Modifier.padding(bottom = 8.dp, top = 20.dp).verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
@@ -52,11 +56,33 @@ fun OverlaySettingsUi(
KeyboardShortcutInfoLabel()
- CheckboxSection(
+ CustomBodyCheckboxSection(
title = "FPS",
options = availableOptions.filterOptions(SensorType.Framerate, SensorType.Frametime),
- onOptionToggle = onOptionsToggle,
- onSwitchToggle = { onSectionSwitchToggle(SectionType.Fps, it) }
+ onSwitchToggle = { onSectionSwitchToggle(SectionType.Fps, it) },
+ body = { options ->
+ Column(modifier = Modifier, verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ options.forEach { option ->
+ CheckboxWithLabel(
+ label = option.name,
+ enabled = option.useCheckbox,
+ onCheckedChange = { onOptionsToggle(option.copy(isSelected = !option.isSelected)) },
+ checked = option.isSelected,
+ )
+ }
+ val presentMonApps = getPresentMonApps()
+ if (presentMonApps.isNotEmpty()) {
+ DropdownMenu(
+ label = "Monitored app:",
+ disclaimer = "Apps are auto updated every 10 seconds.",
+ options = presentMonApps,
+ selectedIndex = 0,
+ onValueChanged = { onFpsApplicationSelect(presentMonApps[it]) },
+ modifier = Modifier.padding(top = 8.dp)
+ )
+ }
+ }
+ }
)
CustomBodyCheckboxSection(
@@ -73,29 +99,29 @@ fun OverlaySettingsUi(
options.forEach { option ->
val readings = getGpuSensorReadings().filter { it.SensorType == option.dataType }
- Column(horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxWidth()) {
- CheckboxWithLabel(
- label = option.name,
- enabled = option.useCheckbox,
- onCheckedChange = { onOptionsToggle(option.copy(isSelected = !option.isSelected)) },
- checked = option.isSelected,
- )
+ Column(horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxWidth()) {
+ CheckboxWithLabel(
+ label = option.name,
+ enabled = option.useCheckbox,
+ onCheckedChange = { onOptionsToggle(option.copy(isSelected = !option.isSelected)) },
+ checked = option.isSelected,
+ )
- if (readings.isNotEmpty() && option.isSelected && option.useCustomSensor) {
- SensorReadingDropdownMenu(
- options = readings,
- onValueChanged = {
- onCustomSensorSelect(option.type, it.Identifier)
- },
- selectedIndex = readings
- .indexOfFirst { it.Identifier == option.optionReadingId }
- .coerceAtLeast(0),
- label = "Sensor:",
- sensorName = option.name,
- )
- }
+ if (readings.isNotEmpty() && option.isSelected && option.useCustomSensor) {
+ SensorReadingDropdownMenu(
+ options = readings,
+ onValueChanged = {
+ onCustomSensorSelect(option.type, it.Identifier)
+ },
+ selectedIndex = readings
+ .indexOfFirst { it.Identifier == option.optionReadingId }
+ .coerceAtLeast(0),
+ label = "Sensor:",
+ sensorName = option.name,
+ )
}
}
+ }
}
}
)
@@ -109,27 +135,27 @@ fun OverlaySettingsUi(
options.forEach { option ->
val readings = getCpuSensorReadings().filter { it.SensorType == option.dataType }
- Column(horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxWidth()) {
- CheckboxWithLabel(
- label = option.name,
- onCheckedChange = { onOptionsToggle(option.copy(isSelected = !option.isSelected)) },
- checked = option.isSelected,
+ Column(horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxWidth()) {
+ CheckboxWithLabel(
+ label = option.name,
+ onCheckedChange = { onOptionsToggle(option.copy(isSelected = !option.isSelected)) },
+ checked = option.isSelected,
+ )
+ if (readings.isNotEmpty() && option.isSelected && option.useCustomSensor) {
+ SensorReadingDropdownMenu(
+ options = readings,
+ onValueChanged = {
+ onCustomSensorSelect(option.type, it.Identifier)
+ },
+ selectedIndex = readings
+ .indexOfFirst { it.Identifier == option.optionReadingId }
+ .coerceAtLeast(0),
+ label = "Sensor:",
+ sensorName = option.name,
)
- if (readings.isNotEmpty() && option.isSelected && option.useCustomSensor) {
- SensorReadingDropdownMenu(
- options = readings,
- onValueChanged = {
- onCustomSensorSelect(option.type, it.Identifier)
- },
- selectedIndex = readings
- .indexOfFirst { it.Identifier == option.optionReadingId }
- .coerceAtLeast(0),
- label = "Sensor:",
- sensorName = option.name,
- )
- }
}
}
+ }
}
}
)
@@ -152,34 +178,34 @@ fun OverlaySettingsUi(
body = { options ->
Column(modifier = Modifier, verticalArrangement = Arrangement.spacedBy(12.dp)) {
options.forEach { option ->
- val readings = getNetworkSensorReadings().sortedBy { it.HardwareIdentifier }.filter { it.SensorType == option.dataType }
+ val readings = getNetworkSensorReadings().sortedBy { it.HardwareIdentifier }.filter { it.SensorType == option.dataType }
- Column(horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxWidth()) {
- CheckboxWithLabel(
- label = option.name,
- enabled = option.useCheckbox,
- onCheckedChange = { onOptionsToggle(option.copy(isSelected = !option.isSelected)) },
- checked = option.isSelected,
- )
+ Column(horizontalAlignment = Alignment.Start, modifier = Modifier.fillMaxWidth()) {
+ CheckboxWithLabel(
+ label = option.name,
+ enabled = option.useCheckbox,
+ onCheckedChange = { onOptionsToggle(option.copy(isSelected = !option.isSelected)) },
+ checked = option.isSelected,
+ )
- if (readings.isNotEmpty() && option.isSelected && option.useCustomSensor) {
- SensorReadingDropdownMenu(
- dropdownLabel = {
- "${getHardwareSensors().firstOrNull { hardware -> hardware.Identifier == it.HardwareIdentifier }?.Name}: ${it.Name} (${it.Value} - ${it.SensorType})"
- },
- options = readings,
- onValueChanged = {
- onCustomSensorSelect(option.type, it.Identifier)
- },
- selectedIndex = readings
- .indexOfFirst { it.Identifier == option.optionReadingId }
- .coerceAtLeast(0),
- label = "Sensor:",
- sensorName = option.name,
- )
- }
+ if (readings.isNotEmpty() && option.isSelected && option.useCustomSensor) {
+ SensorReadingDropdownMenu(
+ dropdownLabel = {
+ "${getHardwareSensors().firstOrNull { hardware -> hardware.Identifier == it.HardwareIdentifier }?.Name}: ${it.Name} (${it.Value} - ${it.SensorType})"
+ },
+ options = readings,
+ onValueChanged = {
+ onCustomSensorSelect(option.type, it.Identifier)
+ },
+ selectedIndex = readings
+ .indexOfFirst { it.Identifier == option.optionReadingId }
+ .coerceAtLeast(0),
+ label = "Sensor:",
+ sensorName = option.name,
+ )
}
}
+ }
}
}
)
@@ -193,11 +219,8 @@ fun OverlaySettingsUi(
Text(
text = "May your frames be high, and temps be low.",
- fontSize = 12.sp,
color = LocalColorScheme.current.text.disabled,
- lineHeight = 0.sp,
- fontWeight = FontWeight(550),
- letterSpacing = 0.14.sp,
+ style = LocalTypography.current.labelSMedium,
textAlign = TextAlign.Right,
modifier = Modifier.fillMaxWidth()
)