diff --git a/core/common/src/main/kotlin/mahm/Data.kt b/core/common/src/main/kotlin/mahm/Data.kt index 6879934..8852b2a 100644 --- a/core/common/src/main/kotlin/mahm/Data.kt +++ b/core/common/src/main/kotlin/mahm/Data.kt @@ -21,6 +21,9 @@ val Data.GpuTemp: Int get() = (entries.firstOrNull { it.dwSrcId == SourceID.MONITORING_SOURCE_ID_GPU_TEMPERATURE }?.data?.toInt() ?: 0).coerceAtLeast(1) +val Data.GpuTempUnit: String + get() = entries.firstOrNull { it.dwSrcId == SourceID.MONITORING_SOURCE_ID_GPU_TEMPERATURE }?.szLocalisedSrcUnits ?: "c" + val Data.GpuUsage: Int get() = (entries.firstOrNull { it.dwSrcId == SourceID.MONITORING_SOURCE_ID_GPU_USAGE }?.data?.toInt() ?: 0).coerceAtLeast(1) @@ -37,6 +40,9 @@ val Data.CpuTemp: Int get() = (entries.firstOrNull { it.dwSrcId == SourceID.MONITORING_SOURCE_ID_CPU_TEMPERATURE }?.data?.toInt() ?: 0).coerceAtLeast(1) +val Data.CpuTempUnit: String + get() = entries.firstOrNull { it.dwSrcId == SourceID.MONITORING_SOURCE_ID_CPU_TEMPERATURE }?.szLocalisedSrcUnits ?: "c" + val Data.CpuUsage: Int get() = (entries.firstOrNull { it.dwSrcId == SourceID.MONITORING_SOURCE_ID_CPU_USAGE }?.data?.toInt() ?: 0).coerceAtLeast(1) diff --git a/core/native/src/main/kotlin/mahm/MahmReader.kt b/core/native/src/main/kotlin/mahm/MahmReader.kt index 760f1fb..4795bd9 100644 --- a/core/native/src/main/kotlin/mahm/MahmReader.kt +++ b/core/native/src/main/kotlin/mahm/MahmReader.kt @@ -135,7 +135,7 @@ class MahmReader { val array = ByteArray(MAX_STRING_LENGTH) get(array, 0, MAX_STRING_LENGTH) - return String(trim(array), StandardCharsets.UTF_8) + return String(trim(array), StandardCharsets.ISO_8859_1) } private fun trim(bytes: ByteArray): ByteArray { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6ced2cb..5f951bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = " [libraries] compose-material-icons = "org.jetbrains.compose.material:material-icons-extended-desktop:1.3.1" +compose-material = "org.jetbrains.compose.material3:material3-desktop:1.2.1" ktor-client-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 7b19284..bc663df 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,5 +38,4 @@ rootProject.name = "CleanMeter" include("core:common") include("core:native") -include("target:client") include("target:server") diff --git a/target/server/build.gradle.kts b/target/server/build.gradle.kts index 429e3c1..c53a818 100644 --- a/target/server/build.gradle.kts +++ b/target/server/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(compose.desktop.currentOs) implementation(libs.compose.material.icons) + implementation(libs.compose.material) implementation(projects.core.common) implementation(projects.core.native) @@ -32,10 +33,26 @@ sourceSets { compose.desktop { application { mainClass = "br.com.firstsoft.target.server.ServerMainKt" + + buildTypes.release.proguard { + isEnabled = false + optimize.set(false) + } + + nativeDistributions { - targetFormats(TargetFormat.Msi) + targetFormats(TargetFormat.Exe, TargetFormat.Deb) + packageName = "Clean Meter" packageVersion = "0.0.1" + + windows { + iconFile.set(project.file("src/main/resources/imgs/favicon.ico")) + } + + linux { + iconFile.set(project.file("src/main/resources/imgs/logo.png")) + } } } } \ No newline at end of file diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ServerMain.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ServerMain.kt index f418fc4..f7a4ce3 100644 --- a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ServerMain.kt +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ServerMain.kt @@ -25,7 +25,10 @@ import com.github.kwhat.jnativehook.keyboard.NativeKeyListener import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json import ui.PREFERENCE_START_MINIMIZED +import ui.app.OVERLAY_SETTINGS_PREFERENCE_KEY import ui.app.Overlay import ui.app.OverlaySettings import ui.app.Settings @@ -42,6 +45,20 @@ val positions = listOf( Alignment.BottomEnd, ) +private fun loadOverlaySettings(): OverlaySettings { + val json = PreferencesRepository.getPreferenceString(OVERLAY_SETTINGS_PREFERENCE_KEY) + val settings = if (json != null) { + try { + Json.decodeFromString(json) + } catch (e: Exception) { + OverlaySettings() + } + } else { + OverlaySettings() + } + return settings +} + fun main() { val channel = Channel() @@ -59,13 +76,13 @@ fun main() { }) application { - var overlaySettings by remember { mutableStateOf(OverlaySettings()) } + var overlaySettings by remember { mutableStateOf(loadOverlaySettings()) } OverlayWindow(channel, overlaySettings) - SettingsWindow { + SettingsWindow(overlaySettings, { overlaySettings = it - } + }) } } @@ -81,11 +98,9 @@ private fun ApplicationScope.OverlayWindow( } } - val alignment = remember(overlaySettings.positionIndex) { positions[overlaySettings.positionIndex] } val overlayState = rememberWindowState().apply { size = if (overlaySettings.isHorizontal) DpSize(1024.dp, 80.dp) else DpSize(350.dp, 1024.dp) placement = WindowPlacement.Floating - position = WindowPosition.Aligned(alignment) } val graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment() @@ -105,7 +120,8 @@ private fun ApplicationScope.OverlayWindow( focusable = false, enabled = false, ) { - LaunchedEffect(alignment, overlaySettings.selectedDisplayIndex) { + LaunchedEffect(overlaySettings) { + val alignment = positions[overlaySettings.positionIndex] val location = when (alignment) { Alignment.TopStart -> IntSize(graphicsConfiguration.bounds.x, graphicsConfiguration.bounds.y) Alignment.TopCenter -> IntSize( @@ -135,6 +151,7 @@ private fun ApplicationScope.OverlayWindow( else -> IntSize.Zero } + overlayState.position = WindowPosition.Aligned(alignment) window.setLocation(location.width, location.height) window.toFront() } @@ -146,6 +163,7 @@ private fun ApplicationScope.OverlayWindow( @Composable private fun ApplicationScope.SettingsWindow( + overlaySettings: OverlaySettings, onOverlaySettings: (OverlaySettings) -> Unit ) { var isVisible by remember { @@ -157,7 +175,7 @@ private fun ApplicationScope.SettingsWindow( } val icon = painterResource("imgs/logo.png") val state = rememberWindowState().apply { - size = DpSize(500.dp, 500.dp) + size = DpSize(650.dp, 900.dp) } Window( @@ -166,9 +184,16 @@ private fun ApplicationScope.SettingsWindow( icon = icon, visible = isVisible, title = "Clean Meter", - resizable = false + resizable = false, + undecorated = true, + transparent = true, ) { - Settings(onOverlaySettings = onOverlaySettings) + Settings( + overlaySettings = overlaySettings, + onOverlaySettings = onOverlaySettings, + onCloseRequest = { isVisible = false }, + onMinimizeRequest = { state.isMinimized = true } + ) } if (!isVisible) { diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/ColorTokens.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/ColorTokens.kt new file mode 100644 index 0000000..376c47f --- /dev/null +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/ColorTokens.kt @@ -0,0 +1,17 @@ +package br.com.firstsoft.target.server.ui + +import androidx.compose.ui.graphics.Color + +object ColorTokens { + val Green = Color(0xff1cad69) + val Yellow = Color(0xfffcc748) + val Red = Color(0xffed4335) + val ClearGray = Color(0x11d3d3d3) + val OffWhite = Color(0xffc0c0c0) + + val BackgroundOffWhite = Color(0xffF0F1F1) + val BarelyVisibleGray = Color(0x0C111D1A) + val AlmostVisibleGray = Color(0xFFCECFD2) + val DarkGray = Color(0xFF0C111D) + val MutedGray = Color(0xFF475467) +} \ No newline at end of file diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/Footer.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/Footer.kt deleted file mode 100644 index 7b03586..0000000 --- a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/Footer.kt +++ /dev/null @@ -1,40 +0,0 @@ -package ui - -import Label -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.TooltipArea -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material.Icon -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun FooterUi() { - val uriHandler = LocalUriHandler.current - Row( - modifier = Modifier.fillMaxWidth().height(32.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - TooltipArea({ - Label(text = "Check for updates") - }) { - Icon( - painter = painterResource("imgs/github.svg"), - null, - modifier = Modifier.clickable { - uriHandler.openUri("https://github.com/Danil0v3s/CleanMeter/releases/latest") - } - ) - } - } -} diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/OverlaySettingsUi.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/OverlaySettingsUi.kt deleted file mode 100644 index a8b933b..0000000 --- a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/OverlaySettingsUi.kt +++ /dev/null @@ -1,140 +0,0 @@ -package br.com.firstsoft.target.server.ui - -import Label -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Divider -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 -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import br.com.firstsoft.target.server.ui.components.CheckboxWithLabel -import br.com.firstsoft.target.server.ui.components.DropdownMenu -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import ui.app.OVERLAY_SETTINGS_PREFERENCE_KEY -import ui.app.OverlaySettings -import ui.app.positionsLabels -import java.awt.GraphicsEnvironment - -@Composable -fun OverlaySettingsUi( - onOverlaySettings: (OverlaySettings) -> Unit, -) = Column( - modifier = Modifier - .padding(bottom = 8.dp) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(8.dp) -) { - val screenDevices = remember { GraphicsEnvironment.getLocalGraphicsEnvironment().screenDevices } - - var overlaySettings by remember { - val json = PreferencesRepository.getPreferenceString(OVERLAY_SETTINGS_PREFERENCE_KEY) - val settings = if (json != null) { - try { - Json.decodeFromString(json) - } catch (e: Exception) { - OverlaySettings() - } - } else { - OverlaySettings() - } - mutableStateOf(settings) - } - - LaunchedEffect(overlaySettings) { - PreferencesRepository.setPreference(OVERLAY_SETTINGS_PREFERENCE_KEY, Json.encodeToString(overlaySettings)) - onOverlaySettings(overlaySettings) - } - - DropdownMenu( - title = "Overlay Position", - options = positionsLabels, - selectedIndex = overlaySettings.positionIndex, - onValueChanged = { overlaySettings = overlaySettings.copy(positionIndex = it) } - ) - DropdownMenu( - title = "Selected Display", - options = screenDevices.map { it.defaultConfiguration.device.iDstring }, - selectedIndex = overlaySettings.selectedDisplayIndex, - onValueChanged = { overlaySettings = overlaySettings.copy(selectedDisplayIndex = it) } - ) - Divider() - - DropdownMenu( - title = "Graph Type", - options = OverlaySettings.ProgressType.entries.map { it.name }, - selectedIndex = overlaySettings.progressType.ordinal, - onValueChanged = { - overlaySettings = overlaySettings.copy(progressType = OverlaySettings.ProgressType.entries[it]) - } - ) - Divider() - - Label("Orientation") - CheckboxWithLabel( - label = "Use Horizontal?", - onCheckedChange = { overlaySettings = overlaySettings.copy(isHorizontal = it) }, - checked = overlaySettings.isHorizontal, - ) - Divider() - - Label("FPS Settings") - CheckboxWithLabel( - label = "Frame Count", - onCheckedChange = { overlaySettings = overlaySettings.copy(fps = it) }, - checked = overlaySettings.fps - ) - CheckboxWithLabel( - label = "Frame Time", - onCheckedChange = { overlaySettings = overlaySettings.copy(frametime = it) }, - checked = overlaySettings.frametime - ) - Divider() - - Label("GPU Settings") - CheckboxWithLabel( - label = "GPU Temperature", - onCheckedChange = { overlaySettings = overlaySettings.copy(gpuTemp = it) }, - checked = overlaySettings.gpuTemp - ) - CheckboxWithLabel( - label = "GPU Usage", - onCheckedChange = { overlaySettings = overlaySettings.copy(gpuUsage = it) }, - checked = overlaySettings.gpuUsage - ) - CheckboxWithLabel( - label = "VRAM Usage", - onCheckedChange = { overlaySettings = overlaySettings.copy(vramUsage = it) }, - checked = overlaySettings.vramUsage - ) - Divider() - - Label("CPU Settings") - CheckboxWithLabel( - label = "CPU Temperature", - onCheckedChange = { overlaySettings = overlaySettings.copy(cpuTemp = it) }, - checked = overlaySettings.cpuTemp - ) - CheckboxWithLabel( - label = "CPU Usage", - onCheckedChange = { overlaySettings = overlaySettings.copy(cpuUsage = it) }, - checked = overlaySettings.cpuUsage - ) - Divider() - - Label("RAM Settings") - CheckboxWithLabel( - label = "RAM Usage", - onCheckedChange = { overlaySettings = overlaySettings.copy(ramUsage = it) }, - checked = overlaySettings.ramUsage - ) -} \ No newline at end of file diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/app/Settings.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/app/Settings.kt deleted file mode 100644 index 4075a50..0000000 --- a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/app/Settings.kt +++ /dev/null @@ -1,78 +0,0 @@ -package ui.app - -import Title -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.Tab -import androidx.compose.material.TabRow -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import br.com.firstsoft.target.server.ui.AppTheme -import br.com.firstsoft.target.server.ui.OverlaySettingsUi -import kotlinx.serialization.Serializable -import ui.AppSettings - -const val OVERLAY_SETTINGS_PREFERENCE_KEY = "OVERLAY_SETTINGS_PREFERENCE_KEY" - -val positionsLabels = listOf( - "Top Start", - "Top Center", - "Top End", - "Bottom Start", - "Bottom Center", - "Bottom End", -) - -@Composable -fun Settings( - onOverlaySettings: (OverlaySettings) -> Unit, -) = AppTheme { - Column(modifier = Modifier.fillMaxSize()) { - - var selectedTabIndex by remember { mutableStateOf(0) } - - TabRow( - selectedTabIndex = selectedTabIndex, - modifier = Modifier.fillMaxWidth() - ) { - Tab(selected = selectedTabIndex == 0, onClick = { selectedTabIndex = 0 }) { - Title("Overlay Settings") - } - Tab(selected = selectedTabIndex == 1, onClick = { selectedTabIndex = 1 }) { - Title("App Settings") - } - } - - when (selectedTabIndex) { - 0 -> OverlaySettingsUi(onOverlaySettings) - 1 -> AppSettings() - else -> Unit - } - } -} - -@Serializable -data class OverlaySettings( - val isHorizontal: Boolean = true, - val positionIndex: Int = 0, - val selectedDisplayIndex: Int = 0, - val fps: Boolean = true, - val frametime: Boolean = false, - val cpuTemp: Boolean = true, - val cpuUsage: Boolean = true, - val gpuTemp: Boolean = true, - val gpuUsage: Boolean = true, - val vramUsage: Boolean = false, - val ramUsage: Boolean = false, - val progressType: ProgressType = ProgressType.Circular, -) { - @Serializable - enum class ProgressType { - Circular, Bar, None - } -} diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/CheckboxSection.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/CheckboxSection.kt new file mode 100644 index 0000000..e923ad2 --- /dev/null +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/CheckboxSection.kt @@ -0,0 +1,60 @@ +package br.com.firstsoft.target.server.ui.components + +import androidx.compose.foundation.background +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.shape.RoundedCornerShape +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import br.com.firstsoft.target.server.ui.ColorTokens.MutedGray +import br.com.firstsoft.target.server.ui.tabs.settings.CheckboxSectionOption + +@Composable +fun CheckboxSection( + title: String, + options: List, + onOptionToggle: (CheckboxSectionOption) -> Unit, + onSwitchToggle: (Boolean) -> Unit, +) = Column( + modifier = Modifier.background(Color.White, RoundedCornerShape(12.dp)).padding(20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + fontSize = 13.sp, + color = MutedGray, + lineHeight = 0.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = 1.sp + ) + Toggle( + checked = options.any { it.isSelected }, + onCheckedChange = onSwitchToggle + ) + } + + Column(modifier = Modifier, verticalArrangement = Arrangement.spacedBy(12.dp)) { + options.forEach { option -> + CheckboxWithLabel( + label = option.name, + onCheckedChange = { onOptionToggle(option.copy(isSelected = !option.isSelected)) }, + checked = option.isSelected, + ) + } + } +} \ No newline at end of file diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/CheckboxWithLabel.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/CheckboxWithLabel.kt index 687dd43..d1a4ff5 100644 --- a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/CheckboxWithLabel.kt +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/CheckboxWithLabel.kt @@ -1,15 +1,21 @@ package br.com.firstsoft.target.server.ui.components import Label +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size import androidx.compose.material.Checkbox import androidx.compose.material.CheckboxDefaults import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import br.com.firstsoft.target.server.ui.ColorTokens.DarkGray +import br.com.firstsoft.target.server.ui.ColorTokens.MutedGray @Composable fun CheckboxWithLabel( @@ -20,15 +26,23 @@ fun CheckboxWithLabel( ) { Row( verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Checkbox( checked = checked, onCheckedChange = onCheckedChange, - colors = CheckboxDefaults.colors(checkedColor = MaterialTheme.colors.primary), + colors = CheckboxDefaults.colors(checkedColor = DarkGray), modifier = Modifier.size(24.dp) ) - Label(text = label) + Text( + text = label, + fontSize = 14.sp, + color = DarkGray, + lineHeight = 0.sp, + fontWeight = FontWeight(550), + letterSpacing = 0.14.sp + ) trailingItem?.invoke() } diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/CollapsibleSection.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/CollapsibleSection.kt new file mode 100644 index 0000000..f4d840e --- /dev/null +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/CollapsibleSection.kt @@ -0,0 +1,70 @@ +package br.com.firstsoft.target.server.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +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.rounded.ChevronRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import br.com.firstsoft.target.server.ui.ColorTokens.MutedGray + +@Composable +fun CollapsibleSection( + title: String, + content: @Composable () -> Unit +) = Column( + modifier = Modifier.background(Color.White, RoundedCornerShape(12.dp)).padding(20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) +) { + var expanded by remember { mutableStateOf(false) } + + Row( + modifier = Modifier.fillMaxWidth().clickable { expanded = !expanded }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + fontSize = 13.sp, + color = MutedGray, + lineHeight = 0.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = 1.sp + ) + IconButton(onClick = { expanded = !expanded }, modifier = Modifier.clearAndSetSemantics { } .height(20.dp)) { + Icon( + imageVector = Icons.Rounded.ChevronRight, + contentDescription = "Trailing icon for exposed dropdown menu", + modifier = Modifier + .rotate(if (expanded) 270f else 90f) + .height(20.dp) + ) + } + } + + AnimatedVisibility(expanded) { + content() + } +} \ No newline at end of file diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/DropdownSection.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/DropdownSection.kt new file mode 100644 index 0000000..60216b8 --- /dev/null +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/DropdownSection.kt @@ -0,0 +1,126 @@ +package br.com.firstsoft.target.server.ui.components + +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.shape.RoundedCornerShape +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ExposedDropdownMenuBox +import androidx.compose.material.ExposedDropdownMenuDefaults +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ChevronRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import br.com.firstsoft.target.server.ui.ColorTokens.AlmostVisibleGray +import br.com.firstsoft.target.server.ui.ColorTokens.MutedGray + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun DropdownSection( + title: String, + options: List, + onValueChanged: (Int) -> Unit, + selectedIndex: Int, +) = Column( + modifier = Modifier.background(Color.White, RoundedCornerShape(12.dp)).padding(20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) +) { + var expanded by remember { mutableStateOf(false) } + var selectedOption by remember { mutableStateOf(options[selectedIndex]) } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + fontSize = 13.sp, + color = MutedGray, + lineHeight = 0.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = 1.sp + ) + } + + Column(modifier = Modifier, verticalArrangement = Arrangement.spacedBy(12.dp)) { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + modifier = Modifier.fillMaxWidth(), + ) { + TextField( + value = selectedOption, + onValueChange = {}, + readOnly = true, + trailingIcon = { + IconButton(onClick = { }, modifier = Modifier.clearAndSetSemantics { }) { + Icon( + Icons.Rounded.ChevronRight, + "Trailing icon for exposed dropdown menu", + Modifier.rotate( + if (expanded) + 270f + else + 90f + ) + ) + } + }, + modifier = Modifier.fillMaxWidth().border(1.dp, AlmostVisibleGray, RoundedCornerShape(16.dp)), + textStyle = TextStyle( + color = Color.DarkGray, + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ), + shape = RoundedCornerShape(16.dp), + colors = ExposedDropdownMenuDefaults.textFieldColors( + backgroundColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + trailingIconColor = MutedGray, + focusedTrailingIconColor = MutedGray, + ) + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.fillMaxWidth(), + ) { + options.forEachIndexed { index, item -> + DropdownMenuItem( + onClick = { + expanded = false + selectedOption = item + onValueChanged(index) + }, modifier = Modifier.fillMaxWidth() + ) { + Text(text = item) + } + } + } + } + } +} \ No newline at end of file diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/Pill.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/Pill.kt index 9c50e45..a09da74 100644 --- a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/Pill.kt +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/Pill.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import ui.ColorTokens.OffWhite +import br.com.firstsoft.target.server.ui.ColorTokens.OffWhite import ui.conditional @Composable diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/Progress.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/Progress.kt index 0bedc20..3574091 100644 --- a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/Progress.kt +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/Progress.kt @@ -1,5 +1,6 @@ package br.com.firstsoft.target.server.ui.components +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -20,10 +21,10 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import ui.ColorTokens.ClearGray -import ui.ColorTokens.Green -import ui.ColorTokens.Red -import ui.ColorTokens.Yellow +import br.com.firstsoft.target.server.ui.ColorTokens.ClearGray +import br.com.firstsoft.target.server.ui.ColorTokens.Green +import br.com.firstsoft.target.server.ui.ColorTokens.Red +import br.com.firstsoft.target.server.ui.ColorTokens.Yellow import ui.app.OverlaySettings import kotlin.math.abs @@ -35,8 +36,8 @@ fun Progress( progressType: OverlaySettings.ProgressType ) { val color = when { - value > 0.7f && value < 0.9f -> Red - value > 0.5f && value < 0.7f -> Yellow + value > 0.8f -> Red + value in 0.6f..0.8f -> Yellow else -> Green } Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { @@ -51,8 +52,16 @@ fun Progress( ) OverlaySettings.ProgressType.Bar -> Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + val integerValue = value.times(10).toInt() + repeat(10) { - val barColor = if (value.times(10) > abs(it - 9)) color else Color.Transparent + val inverseValue = abs(it - 9) + + val barColor = if (integerValue < inverseValue) Color.Transparent else when { + inverseValue >= 8 -> Red + inverseValue in 5..7 -> Yellow + else -> Green + } Box( modifier = Modifier @@ -94,4 +103,45 @@ private fun ProgressLabel(label: String) { lineHeight = 0.sp, fontWeight = FontWeight.Normal, ) +} + +@Preview +@Composable +private fun ProgressPreview() { + Row(modifier = Modifier.background(Color.Black)) { + Progress( + value = 0.5f, + label = "05", + progressType = OverlaySettings.ProgressType.Bar, + unit = "C" + ) + + Progress( + value = 0.6f, + label = "06", + progressType = OverlaySettings.ProgressType.Bar, + unit = "C" + ) + + Progress( + value = 0.7f, + label = "07", + progressType = OverlaySettings.ProgressType.Bar, + unit = "C" + ) + + Progress( + value = 0.8f, + label = "08", + progressType = OverlaySettings.ProgressType.Bar, + unit = "C" + ) + + Progress( + value = 0.9f, + label = "09", + progressType = OverlaySettings.ProgressType.Bar, + unit = "C" + ) + } } \ No newline at end of file diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/Section.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/Section.kt new file mode 100644 index 0000000..e703a13 --- /dev/null +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/Section.kt @@ -0,0 +1,49 @@ +package br.com.firstsoft.target.server.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import br.com.firstsoft.target.server.ui.ColorTokens.MutedGray +import br.com.firstsoft.target.server.ui.tabs.settings.CheckboxSectionOption + +@Composable +fun Section( + title: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) = Column( + modifier = modifier.background(Color.White, RoundedCornerShape(12.dp)).padding(20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + fontSize = 13.sp, + color = MutedGray, + lineHeight = 0.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = 1.sp + ) + + } + + content() +} \ No newline at end of file diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/Toggle.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/Toggle.kt new file mode 100644 index 0000000..0853c3f --- /dev/null +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/Toggle.kt @@ -0,0 +1,31 @@ +package br.com.firstsoft.target.server.ui.components + +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import br.com.firstsoft.target.server.ui.ColorTokens.AlmostVisibleGray +import br.com.firstsoft.target.server.ui.ColorTokens.Green + +@Composable +fun Toggle( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) = + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + colors = SwitchDefaults.colors( + checkedThumbColor = Color.White, + checkedTrackColor = Green, + checkedBorderColor = Color.Transparent, + uncheckedThumbColor = Color.White, + uncheckedTrackColor = AlmostVisibleGray, + uncheckedBorderColor = Color.Transparent, + ), + modifier = Modifier.scale(0.7f).height(20.dp), + ) \ No newline at end of file diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/ToggleSection.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/ToggleSection.kt new file mode 100644 index 0000000..6ad17b7 --- /dev/null +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/components/ToggleSection.kt @@ -0,0 +1,50 @@ +package br.com.firstsoft.target.server.ui.components + +import androidx.compose.foundation.background +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.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import br.com.firstsoft.target.server.ui.ColorTokens.MutedGray + +@Composable +fun ToggleSection( + title: String, + isEnabled: Boolean, + onSwitchToggle: (Boolean) -> Unit, + content: @Composable () -> Unit +) = Column( + modifier = Modifier.background(Color.White, RoundedCornerShape(12.dp)).padding(20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + fontSize = 13.sp, + color = MutedGray, + lineHeight = 0.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = 1.sp + ) + Toggle( + checked = isEnabled, + onCheckedChange = onSwitchToggle + ) + } + + content() +} \ No newline at end of file diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/app/Overlay.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/overlay/Overlay.kt similarity index 100% rename from target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/app/Overlay.kt rename to target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/overlay/Overlay.kt diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/OverlayUi.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/overlay/OverlayUi.kt similarity index 97% rename from target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/OverlayUi.kt rename to target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/overlay/OverlayUi.kt index d51c81f..f305b31 100644 --- a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/OverlayUi.kt +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/overlay/OverlayUi.kt @@ -42,30 +42,26 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.util.fastForEachIndexed +import br.com.firstsoft.target.server.ui.ColorTokens.OffWhite import br.com.firstsoft.target.server.ui.components.Pill import br.com.firstsoft.target.server.ui.components.Progress import mahm.CpuTemp +import mahm.CpuTempUnit import mahm.CpuUsage import mahm.Data import mahm.FPS import mahm.Frametime import mahm.GpuTemp +import mahm.GpuTempUnit import mahm.GpuUsage import mahm.MahmReader import mahm.RamUsage import mahm.RamUsagePercent import mahm.VramUsage import mahm.VramUsagePercent -import ui.ColorTokens.OffWhite import ui.app.OverlaySettings -object ColorTokens { - val Green = Color(0xff1cad69) - val Yellow = Color(0xfffcc748) - val Red = Color(0xffed4335) - val ClearGray = Color(0x11d3d3d3) - val OffWhite = Color(0xffc0c0c0) -} + inline fun Modifier.conditional( predicate: Boolean, @@ -199,7 +195,7 @@ private fun cpu(overlaySettings: OverlaySettings, data: Data) { Progress( value = data.CpuTemp / 100f, label = "${data.CpuTemp}", - unit = "c", + unit = data.CpuTempUnit, progressType = overlaySettings.progressType ) } @@ -226,7 +222,7 @@ private fun gpu(overlaySettings: OverlaySettings, data: Data) { Progress( value = data.GpuTemp / 100f, label = "${data.GpuTemp}", - unit = "c", + unit = data.GpuTempUnit, progressType = overlaySettings.progressType ) } diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/SettingsUi.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/AppSettingsUi.kt similarity index 71% rename from target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/SettingsUi.kt rename to target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/AppSettingsUi.kt index 385c226..eb2262a 100644 --- a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/SettingsUi.kt +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/AppSettingsUi.kt @@ -16,25 +16,38 @@ import androidx.compose.ui.unit.dp import PreferencesRepository import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import br.com.firstsoft.target.server.ui.components.CheckboxWithLabel +import br.com.firstsoft.target.server.ui.components.Section import win32.WinRegistry const val PREFERENCE_START_MINIMIZED = "PREFERENCE_START_MINIMIZED" @Composable -fun AppSettings() = Column { +fun AppSettingsUi() = Box(modifier = Modifier.fillMaxSize().padding(top = 20.dp)) { Column( - modifier = Modifier.weight(1f).fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp) + modifier = Modifier.verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - startWithWindowsCheckbox() - startMinimizedCheckbox() - } - Box(modifier = Modifier.weight(0.1f)) { - FooterUi() + Section( + title = "GENERAL", + ) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + startWithWindowsCheckbox() + startMinimizedCheckbox() + } + } } + + FooterUi(modifier = Modifier.align(Alignment.BottomStart)) } @OptIn(ExperimentalFoundationApi::class) diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/CheckboxSectionOption.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/CheckboxSectionOption.kt new file mode 100644 index 0000000..c92a83f --- /dev/null +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/CheckboxSectionOption.kt @@ -0,0 +1,9 @@ +package br.com.firstsoft.target.server.ui.tabs.settings + +data class CheckboxSectionOption( + val isSelected: Boolean, val name: String, val type: SettingsOptionType +) + +enum class SettingsOptionType { + Framerate, Frametime, CpuTemp, CpuUsage, GpuTemp, GpuUsage, VramUsage, RamUsage, +} \ No newline at end of file diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/Footer.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/Footer.kt new file mode 100644 index 0000000..0f75584 --- /dev/null +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/Footer.kt @@ -0,0 +1,69 @@ +package ui + +import Label +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.TooltipArea +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import br.com.firstsoft.target.server.ui.ColorTokens.AlmostVisibleGray +import br.com.firstsoft.target.server.ui.ColorTokens.DarkGray +import br.com.firstsoft.target.server.ui.ColorTokens.MutedGray + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun FooterUi(modifier: Modifier = Modifier) { + val uriHandler = LocalUriHandler.current + Row( + modifier = modifier.fillMaxWidth().height(32.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "May your frames be high, and temps be low.", + fontSize = 12.sp, + color = AlmostVisibleGray, + lineHeight = 0.sp, + fontWeight = FontWeight(550), + letterSpacing = 0.14.sp + ) + + TooltipArea({ + Label(text = "Check for updates") + }) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.clickable { + uriHandler.openUri("https://github.com/Danil0v3s/CleanMeter/releases/latest") + }) { + Icon( + painter = painterResource("imgs/github.svg"), + null, + tint = AlmostVisibleGray, + ) + + Text( + text = "Version ${System.getProperty("jpackage.app-version")}", + fontSize = 14.sp, + color = AlmostVisibleGray, + lineHeight = 0.sp, + fontWeight = FontWeight(550), + letterSpacing = 0.14.sp, + ) + } + } + } +} diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/OverlaySettingsUi.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/OverlaySettingsUi.kt new file mode 100644 index 0000000..1f079e6 --- /dev/null +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/OverlaySettingsUi.kt @@ -0,0 +1,124 @@ +package br.com.firstsoft.target.server.ui.tabs.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import br.com.firstsoft.target.server.ui.components.CheckboxSection +import br.com.firstsoft.target.server.ui.components.DropdownSection +import ui.app.OverlaySettings +import java.awt.GraphicsEnvironment + +private fun List.filterOptions(vararg optionType: SettingsOptionType) = + this.filter { source -> optionType.any { it == source.type } } + +@Composable +fun OverlaySettingsUi( + overlaySettings: OverlaySettings, + onOverlaySettings: (OverlaySettings) -> Unit, +) = Column( + modifier = Modifier.padding(bottom = 8.dp, top = 20.dp).verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) +) { + val screenDevices = remember { GraphicsEnvironment.getLocalGraphicsEnvironment().screenDevices } + + val availableOptions = remember(overlaySettings) { + listOf( + CheckboxSectionOption( + isSelected = overlaySettings.fps, + name = "Frame count", + type = SettingsOptionType.Framerate + ), + CheckboxSectionOption( + isSelected = overlaySettings.frametime, + name = "Frame time graph", + type = SettingsOptionType.Frametime + ), + CheckboxSectionOption( + isSelected = overlaySettings.cpuTemp, + name = "CPU temperature", + type = SettingsOptionType.CpuTemp + ), + CheckboxSectionOption( + isSelected = overlaySettings.cpuUsage, + name = "CPU usage", + type = SettingsOptionType.CpuUsage + ), + CheckboxSectionOption( + isSelected = overlaySettings.gpuTemp, + name = "GPU temperature", + type = SettingsOptionType.GpuTemp + ), + CheckboxSectionOption( + isSelected = overlaySettings.gpuUsage, + name = "GPU usage", + type = SettingsOptionType.GpuUsage + ), + CheckboxSectionOption( + isSelected = overlaySettings.vramUsage, + name = "Vram usage", + type = SettingsOptionType.VramUsage + ), + CheckboxSectionOption( + isSelected = overlaySettings.ramUsage, + name = "Ram usage", + type = SettingsOptionType.RamUsage + ), + ) + } + + fun onOptionsToggle(option: CheckboxSectionOption) { + val newSettings = when (option.type) { + SettingsOptionType.Framerate -> overlaySettings.copy(fps = option.isSelected) + SettingsOptionType.Frametime -> overlaySettings.copy(frametime = option.isSelected) + SettingsOptionType.CpuTemp -> overlaySettings.copy(cpuTemp = option.isSelected) + SettingsOptionType.CpuUsage -> overlaySettings.copy(cpuUsage = option.isSelected) + SettingsOptionType.GpuTemp -> overlaySettings.copy(gpuTemp = option.isSelected) + SettingsOptionType.GpuUsage -> overlaySettings.copy(gpuUsage = option.isSelected) + SettingsOptionType.VramUsage -> overlaySettings.copy(vramUsage = option.isSelected) + SettingsOptionType.RamUsage -> overlaySettings.copy(ramUsage = option.isSelected) + } + onOverlaySettings(newSettings) + } + + CheckboxSection(title = "FPS", + options = availableOptions.filterOptions(SettingsOptionType.Framerate, SettingsOptionType.Frametime), + onOptionToggle = ::onOptionsToggle, + onSwitchToggle = { + onOverlaySettings(overlaySettings.copy(fps = it, frametime = it)) + } + ) + + CheckboxSection(title = "GPU", + options = availableOptions.filterOptions( + SettingsOptionType.GpuUsage, + SettingsOptionType.GpuTemp, + SettingsOptionType.VramUsage + ), + onOptionToggle = ::onOptionsToggle, + onSwitchToggle = { onOverlaySettings(overlaySettings.copy(gpuTemp = it, gpuUsage = it, vramUsage = it)) } + ) + + CheckboxSection(title = "CPU", + options = availableOptions.filterOptions(SettingsOptionType.CpuUsage, SettingsOptionType.CpuTemp), + onOptionToggle = ::onOptionsToggle, + onSwitchToggle = { onOverlaySettings(overlaySettings.copy(cpuTemp = it, cpuUsage = it)) } + ) + + CheckboxSection(title = "RAM", + options = availableOptions.filterOptions(SettingsOptionType.RamUsage), + onOptionToggle = ::onOptionsToggle, + onSwitchToggle = { onOverlaySettings(overlaySettings.copy(ramUsage = it)) } + ) + + DropdownSection(title = "MONITOR", + options = screenDevices.map { it.defaultConfiguration.device.iDstring }, + selectedIndex = overlaySettings.selectedDisplayIndex, + onValueChanged = { onOverlaySettings(overlaySettings.copy(selectedDisplayIndex = it)) } + ) +} \ No newline at end of file diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/Settings.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/Settings.kt new file mode 100644 index 0000000..52892cd --- /dev/null +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/Settings.kt @@ -0,0 +1,241 @@ +package ui.app + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.window.WindowDraggableArea +import androidx.compose.material.ScrollableTabRow +import androidx.compose.material.Tab +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Minimize +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 +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.WindowScope +import br.com.firstsoft.target.server.ui.AppTheme +import br.com.firstsoft.target.server.ui.ColorTokens.BackgroundOffWhite +import br.com.firstsoft.target.server.ui.ColorTokens.BarelyVisibleGray +import br.com.firstsoft.target.server.ui.ColorTokens.DarkGray +import br.com.firstsoft.target.server.ui.ColorTokens.MutedGray +import br.com.firstsoft.target.server.ui.tabs.settings.OverlaySettingsUi +import br.com.firstsoft.target.server.ui.tabs.settings.StyleUi +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import ui.AppSettingsUi + +const val OVERLAY_SETTINGS_PREFERENCE_KEY = "OVERLAY_SETTINGS_PREFERENCE_KEY" + +val positionsLabels = listOf( + "Top Start", + "Top Center", + "Top End", + "Bottom Start", + "Bottom Center", + "Bottom End", +) + +@Composable +fun WindowScope.Settings( + overlaySettings: OverlaySettings, + onCloseRequest: () -> Unit, + onMinimizeRequest: () -> Unit, + onOverlaySettings: (OverlaySettings) -> Unit, +) = AppTheme { + LaunchedEffect(overlaySettings) { + PreferencesRepository.setPreference(OVERLAY_SETTINGS_PREFERENCE_KEY, Json.encodeToString(overlaySettings)) + onOverlaySettings(overlaySettings) + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(BackgroundOffWhite, RoundedCornerShape(12.dp)) + ) { + WindowDraggableArea { + TopBar(onCloseRequest = onCloseRequest, onMinimizeRequest = onMinimizeRequest) + } + + var selectedTabIndex by remember { mutableStateOf(0) } + + Column(modifier = Modifier.fillMaxSize().padding(24.dp)) { + ScrollableTabRow( + selectedTabIndex = selectedTabIndex, + modifier = Modifier.fillMaxWidth().height(44.dp), + backgroundColor = Color.Transparent, + contentColor = DarkGray, + edgePadding = 0.dp, + indicator = { tabPositions -> }, + divider = {} + ) { + SettingsTab( + selected = selectedTabIndex == 0, + onClick = { selectedTabIndex = 0 }, + label = "Stats", + icon = painterResource("icons/data_usage.svg"), + ) + SettingsTab( + selected = selectedTabIndex == 1, + onClick = { selectedTabIndex = 1 }, + label = "Style", + icon = painterResource("icons/layers.svg"), + modifier = Modifier.padding(start = 8.dp) + ) + SettingsTab( + selected = selectedTabIndex == 2, + onClick = { selectedTabIndex = 2 }, + label = "Settings", + icon = painterResource("icons/settings.svg"), + modifier = Modifier.padding(start = 8.dp), + ) + } + + when (selectedTabIndex) { + 0 -> OverlaySettingsUi(overlaySettings, onOverlaySettings) + 1 -> StyleUi(overlaySettings, onOverlaySettings) + 2 -> AppSettingsUi() + else -> Unit + } + } + } +} + +@Composable +private fun SettingsTab( + selected: Boolean, + onClick: () -> Unit, + label: String, + icon: Painter, + modifier: Modifier = Modifier, +) = Tab( + selected = selected, + onClick = onClick, + selectedContentColor = DarkGray, + unselectedContentColor = MutedGray, + modifier = modifier + .fillMaxHeight() + .background( + color = if (selected) DarkGray else Color.White, + shape = RoundedCornerShape(50) + ) + .border(2.dp, BarelyVisibleGray, RoundedCornerShape(50)) + .padding(horizontal = 16.dp), +) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize() + ) { + Image( + painter = icon, + contentDescription = "logo", + modifier = Modifier.size(16.dp), + colorFilter = ColorFilter.tint(if (selected) Color.White else MutedGray), + ) + Text( + text = label, + fontWeight = FontWeight.Medium, + color = if (selected) Color.White else MutedGray, + fontSize = 16.sp, + ) + } +} + +@Composable +private fun TopBar( + onCloseRequest: () -> Unit, + onMinimizeRequest: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(57.dp) + .drawBehind { + val y = size.height - 2.dp.toPx() / 2 + drawLine( + color = BarelyVisibleGray, + start = Offset(0f, y), + end = Offset(size.width, y), + strokeWidth = 2.dp.toPx() + ) + } + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource("imgs/logo.png"), + contentDescription = "logo", + modifier = Modifier.size(25.dp), + ) + Text( + text = "Clean Meter", + fontWeight = FontWeight.Medium, + fontSize = 16.sp + ) + } + + Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) { + Image( + imageVector = Icons.Rounded.Minimize, + contentDescription = "Minimize", + colorFilter = ColorFilter.tint(MutedGray), + modifier = Modifier.clickable { onMinimizeRequest() } + ) + Image( + imageVector = Icons.Rounded.Close, + contentDescription = "Close", + colorFilter = ColorFilter.tint(MutedGray), + modifier = Modifier.clickable { onCloseRequest() } + ) + } + } +} + +@Serializable +data class OverlaySettings( + val isHorizontal: Boolean = true, + val positionIndex: Int = 0, + val selectedDisplayIndex: Int = 0, + val fps: Boolean = true, + val frametime: Boolean = false, + val cpuTemp: Boolean = true, + val cpuUsage: Boolean = true, + val gpuTemp: Boolean = true, + val gpuUsage: Boolean = true, + val vramUsage: Boolean = false, + val ramUsage: Boolean = false, + val progressType: ProgressType = ProgressType.Circular, +) { + @Serializable + enum class ProgressType { + Circular, Bar, None + } +} diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/StyleUi.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/StyleUi.kt new file mode 100644 index 0000000..eb84852 --- /dev/null +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ui/tabs/settings/StyleUi.kt @@ -0,0 +1,287 @@ +package br.com.firstsoft.target.server.ui.tabs.settings + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import br.com.firstsoft.target.server.ui.ColorTokens.AlmostVisibleGray +import br.com.firstsoft.target.server.ui.ColorTokens.BarelyVisibleGray +import br.com.firstsoft.target.server.ui.ColorTokens.DarkGray +import br.com.firstsoft.target.server.ui.ColorTokens.MutedGray +import br.com.firstsoft.target.server.ui.components.CollapsibleSection +import br.com.firstsoft.target.server.ui.components.ToggleSection +import ui.app.OverlaySettings +import ui.conditional + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun StyleUi( + overlaySettings: OverlaySettings, + onOverlaySettings: (OverlaySettings) -> Unit +) = Column( + modifier = Modifier.padding(bottom = 8.dp, top = 20.dp).verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) +) { + CollapsibleSection(title = "POSITION") { + FlowRow( + maxItemsInEachRow = 3, + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + StyleCard( + label = "Top left", + isSelected = overlaySettings.positionIndex == 0, + modifier = Modifier.weight(.3f), + onClick = { onOverlaySettings(overlaySettings.copy(positionIndex = 0)) }, + content = { + Box( + modifier = Modifier + .width(50.dp) + .height(20.dp) + .background( + if (overlaySettings.positionIndex == 0) DarkGray else MutedGray, + RoundedCornerShape(50) + ) + .align(Alignment.TopStart) + ) + } + ) + + StyleCard( + label = "Top middle", + isSelected = overlaySettings.positionIndex == 1, + modifier = Modifier.weight(.3f), + onClick = { onOverlaySettings(overlaySettings.copy(positionIndex = 1)) }, + content = { + Box( + modifier = Modifier + .width(50.dp) + .height(20.dp) + .background( + if (overlaySettings.positionIndex == 1) DarkGray else MutedGray, + RoundedCornerShape(50) + ) + .align(Alignment.TopCenter) + ) + } + ) + + StyleCard( + label = "Top right", + isSelected = overlaySettings.positionIndex == 2, + modifier = Modifier.weight(.3f), + onClick = { onOverlaySettings(overlaySettings.copy(positionIndex = 2)) }, + content = { + Box( + modifier = Modifier + .width(50.dp) + .height(20.dp) + .background( + if (overlaySettings.positionIndex == 1) DarkGray else MutedGray, + RoundedCornerShape(50) + ) + .align(Alignment.TopEnd) + ) + } + ) + + StyleCard( + label = "Bottom left", + isSelected = overlaySettings.positionIndex == 3, + modifier = Modifier.weight(.3f), + onClick = { onOverlaySettings(overlaySettings.copy(positionIndex = 3)) }, + content = { + Box( + modifier = Modifier + .width(50.dp) + .height(20.dp) + .background( + if (overlaySettings.positionIndex == 1) DarkGray else MutedGray, + RoundedCornerShape(50) + ) + .align(Alignment.BottomStart) + ) + } + ) + + StyleCard( + label = "Bottom middle", + isSelected = overlaySettings.positionIndex == 4, + modifier = Modifier.weight(.3f), + onClick = { onOverlaySettings(overlaySettings.copy(positionIndex = 4)) }, + content = { + Box( + modifier = Modifier + .width(50.dp) + .height(20.dp) + .background( + if (overlaySettings.positionIndex == 1) DarkGray else MutedGray, + RoundedCornerShape(50) + ) + .align(Alignment.BottomCenter) + ) + } + ) + + StyleCard( + label = "Bottom right", + isSelected = overlaySettings.positionIndex == 5, + modifier = Modifier.weight(.3f), + onClick = { onOverlaySettings(overlaySettings.copy(positionIndex = 5)) }, + content = { + Box( + modifier = Modifier + .width(50.dp) + .height(20.dp) + .background( + if (overlaySettings.positionIndex == 1) DarkGray else MutedGray, + RoundedCornerShape(50) + ) + .align(Alignment.BottomEnd) + ) + } + ) + } + } + + CollapsibleSection(title = "ORIENTATION") { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + StyleCard( + label = "Horizontal", + isSelected = overlaySettings.isHorizontal, + modifier = Modifier.weight(.5f), + onClick = { onOverlaySettings(overlaySettings.copy(isHorizontal = true)) }, + content = { + Image( + painter = painterResource("icons/horizontal.png"), + contentDescription = "Horizontal image" + ) + } + ) + + StyleCard( + label = "Vertical", + isSelected = !overlaySettings.isHorizontal, + modifier = Modifier.weight(.5f), + onClick = { onOverlaySettings(overlaySettings.copy(isHorizontal = false)) }, + content = { + Image( + painter = painterResource("icons/vertical.png"), + contentDescription = "Vertical image", + modifier = Modifier.align(Alignment.Center) + ) + } + ) + } + } + + ToggleSection( + title = "GRAPH", + isEnabled = overlaySettings.progressType != OverlaySettings.ProgressType.None, + onSwitchToggle = { + if (!it) { + onOverlaySettings(overlaySettings.copy(progressType = OverlaySettings.ProgressType.None)) + } + } + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + StyleCard( + label = "Ring graph", + isSelected = overlaySettings.progressType == OverlaySettings.ProgressType.Circular, + modifier = Modifier.weight(.5f), + onClick = { onOverlaySettings(overlaySettings.copy(progressType = OverlaySettings.ProgressType.Circular)) }, + content = { + Image( + painter = painterResource("icons/rings.png"), + contentDescription = "Horizontal image" + ) + } + ) + + StyleCard( + label = "Bar graph", + isSelected = overlaySettings.progressType == OverlaySettings.ProgressType.Bar, + modifier = Modifier.weight(.5f), + onClick = { onOverlaySettings(overlaySettings.copy(progressType = OverlaySettings.ProgressType.Bar)) }, + content = { + Image( + painter = painterResource("icons/bars.png"), + contentDescription = "Vertical image", + modifier = Modifier.align(Alignment.Center) + ) + } + ) + } + } +} + +@Composable +private fun StyleCard( + label: String, + isSelected: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit, + content: @Composable BoxScope.() -> Unit, +) = Column( + modifier = modifier + .aspectRatio(1.15f) + .background(Color.White, RoundedCornerShape(8.dp)) + .conditional( + predicate = isSelected, + ifTrue = { border(2.dp, DarkGray, RoundedCornerShape(8.dp)) }, + ifFalse = { border(1.dp, AlmostVisibleGray, RoundedCornerShape(8.dp)) } + ) + .padding(4.dp) + .clickable { onClick() } +) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(16 / 9f) + .background(BarelyVisibleGray, RoundedCornerShape(4.dp)) + .padding(8.dp), + content = content, + contentAlignment = Alignment.Center + ) + + Box(modifier = Modifier.weight(1f).fillMaxWidth().padding(12.dp)) { + Text( + text = label, + fontSize = 14.sp, + color = DarkGray, + lineHeight = 0.sp, + fontWeight = FontWeight(550), + letterSpacing = 0.14.sp, + modifier = Modifier.align(Alignment.CenterStart) + ) + } +} + diff --git a/target/server/src/main/resources/icons/bars.png b/target/server/src/main/resources/icons/bars.png new file mode 100644 index 0000000..a1ddb79 Binary files /dev/null and b/target/server/src/main/resources/icons/bars.png differ diff --git a/target/server/src/main/resources/icons/data_usage.svg b/target/server/src/main/resources/icons/data_usage.svg new file mode 100644 index 0000000..61d4a5d --- /dev/null +++ b/target/server/src/main/resources/icons/data_usage.svg @@ -0,0 +1,3 @@ + + + diff --git a/target/server/src/main/resources/icons/horizontal.png b/target/server/src/main/resources/icons/horizontal.png new file mode 100644 index 0000000..30aa223 Binary files /dev/null and b/target/server/src/main/resources/icons/horizontal.png differ diff --git a/target/server/src/main/resources/icons/layers.svg b/target/server/src/main/resources/icons/layers.svg new file mode 100644 index 0000000..aec0fce --- /dev/null +++ b/target/server/src/main/resources/icons/layers.svg @@ -0,0 +1,3 @@ + + + diff --git a/target/server/src/main/resources/icons/rings.png b/target/server/src/main/resources/icons/rings.png new file mode 100644 index 0000000..989c797 Binary files /dev/null and b/target/server/src/main/resources/icons/rings.png differ diff --git a/target/server/src/main/resources/icons/settings.svg b/target/server/src/main/resources/icons/settings.svg new file mode 100644 index 0000000..8cbc6b4 --- /dev/null +++ b/target/server/src/main/resources/icons/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/target/server/src/main/resources/icons/vertical.png b/target/server/src/main/resources/icons/vertical.png new file mode 100644 index 0000000..d071b50 Binary files /dev/null and b/target/server/src/main/resources/icons/vertical.png differ diff --git a/target/server/src/main/resources/imgs/favicon.ico b/target/server/src/main/resources/imgs/favicon.ico new file mode 100644 index 0000000..71275e6 Binary files /dev/null and b/target/server/src/main/resources/imgs/favicon.ico differ