diff --git a/FloconAndroid/app/src/main/AndroidManifest.xml b/FloconAndroid/app/src/main/AndroidManifest.xml index 8131109c0..9bec47652 100644 --- a/FloconAndroid/app/src/main/AndroidManifest.xml +++ b/FloconAndroid/app/src/main/AndroidManifest.xml @@ -3,6 +3,9 @@ xmlns:tools="http://schemas.android.com/tools"> + + + Unit, alwaysOnTop: Boolean = false, - content: @Composable () -> Unit + content: @Composable FrameWindowScope.() -> Unit, ) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DI.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DI.kt new file mode 100644 index 000000000..54403cb1e --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DI.kt @@ -0,0 +1,8 @@ +package io.github.openflocon.flocondesktop.device + +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +internal val deviceModule = module { + viewModelOf(::DeviceViewModel) +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceAction.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceAction.kt new file mode 100644 index 000000000..f951068d9 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceAction.kt @@ -0,0 +1,11 @@ +package io.github.openflocon.flocondesktop.device + +internal sealed interface DeviceAction { + + data class SelectTab(val selected: DeviceTab) : DeviceAction + + data object Refresh : DeviceAction + + data class ChangePermission(val permission: String, val granted: Boolean) : DeviceAction + +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceScreen.kt new file mode 100644 index 000000000..3dd84091a --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceScreen.kt @@ -0,0 +1,176 @@ +package io.github.openflocon.flocondesktop.device + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.openflocon.domain.device.models.DeviceId +import io.github.openflocon.flocondesktop.common.ui.window.FloconWindow +import io.github.openflocon.flocondesktop.common.ui.window.createFloconWindowState +import io.github.openflocon.flocondesktop.device.models.DeviceUiState +import io.github.openflocon.flocondesktop.device.models.previewDeviceUiState +import io.github.openflocon.flocondesktop.device.pages.BatteryPage +import io.github.openflocon.flocondesktop.device.pages.CpuPage +import io.github.openflocon.flocondesktop.device.pages.InfoPage +import io.github.openflocon.flocondesktop.device.pages.MemoryPage +import io.github.openflocon.flocondesktop.device.pages.PermissionPage +import io.github.openflocon.library.designsystem.FloconTheme +import io.github.openflocon.library.designsystem.components.FloconHorizontalDivider +import io.github.openflocon.library.designsystem.components.FloconIconButton +import io.github.openflocon.library.designsystem.components.FloconScaffold +import io.github.openflocon.library.designsystem.components.FloconScrollableTabRow +import io.github.openflocon.library.designsystem.components.FloconSurface +import io.github.openflocon.library.designsystem.components.FloconTab +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.parameter.parametersOf + +@Composable +internal fun DeviceScreen( + deviceId: DeviceId, + onCloseRequest: () -> Unit +) { + val viewModel = koinViewModel { + parametersOf(deviceId) + } + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + Content( + uiState = uiState, + onCloseRequest = onCloseRequest, + onAction = viewModel::onAction + ) +} + +@Composable +private fun Content( + uiState: DeviceUiState, + onCloseRequest: () -> Unit, + onAction: (DeviceAction) -> Unit +) { + val tabs = remember { DeviceTab.entries } + val pagerState = rememberPagerState { tabs.size } + + LaunchedEffect(uiState.contentState.selectedTab) { + pagerState.animateScrollToPage(uiState.contentState.selectedTab.ordinal) + } + + FloconWindow( + title = "Device - ${uiState.infoState.model}", + onCloseRequest = onCloseRequest, + state = createFloconWindowState() + ) { +// window.minimumSize = Dimension(500, 500) // TODO + FloconSurface( + modifier = Modifier.fillMaxSize() + ) { + FloconScaffold( + topBar = { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Header( + uiState = uiState, + onAction = onAction + ) + FloconScrollableTabRow( + selectedTabIndex = uiState.contentState.selectedTab.ordinal, + modifier = Modifier.fillMaxWidth() + ) { + tabs.forEach { tab -> + FloconTab( + text = tab.title, + selected = uiState.contentState.selectedTab == tab, + onClick = { onAction(DeviceAction.SelectTab(tab)) }, + selectedContentColor = FloconTheme.colorPalette.onSurface + ) + } + } + FloconHorizontalDivider( + modifier = Modifier.fillMaxWidth(), + color = FloconTheme.colorPalette.primary + ) + } + } + ) { + HorizontalPager( + state = pagerState, + userScrollEnabled = false, + contentPadding = PaddingValues(8.dp), + pageSpacing = 8.dp, + modifier = Modifier + .fillMaxSize() + .padding(it) + ) { index -> + when (tabs[index]) { + DeviceTab.INFORMATION -> InfoPage(uiState.infoState) + DeviceTab.BATTERY -> BatteryPage(uiState.batteryState) + DeviceTab.CPU -> CpuPage(uiState.cpuState, onAction) + DeviceTab.MEMORY -> MemoryPage(uiState.memoryState) + DeviceTab.PERMISSION -> PermissionPage(uiState.permissionState, onAction) + } + } + } + } + } +} + +@Composable +private fun Header( + uiState: DeviceUiState, + onAction: (DeviceAction) -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .weight(1f) + ) { + Text( + text = uiState.infoState.model, + style = FloconTheme.typography.headlineSmall + ) + SelectionContainer { + Text( + text = uiState.infoState.serialNumber, + style = FloconTheme.typography.labelSmall + ) + } + } + FloconIconButton( + imageVector = Icons.Outlined.Refresh, + onClick = { onAction(DeviceAction.Refresh) } + ) + } +} + +@Composable +@Preview +private fun Preview() { + FloconTheme { + Content( + uiState = previewDeviceUiState(), + onCloseRequest = {}, + onAction = {} + ) + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceTab.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceTab.kt new file mode 100644 index 000000000..2e721e1d4 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceTab.kt @@ -0,0 +1,18 @@ +package io.github.openflocon.flocondesktop.device + +enum class DeviceTab { + INFORMATION, + BATTERY, + CPU, + MEMORY, + PERMISSION +} + +val DeviceTab.title: String + get() = when (this) { + DeviceTab.INFORMATION -> "Info" + DeviceTab.BATTERY -> "Battery" + DeviceTab.CPU -> "CPU" + DeviceTab.MEMORY -> "Memory" + DeviceTab.PERMISSION -> "Permission" + } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceViewModel.kt new file mode 100644 index 000000000..0ea79d68c --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceViewModel.kt @@ -0,0 +1,338 @@ +package io.github.openflocon.flocondesktop.device + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.openflocon.domain.adb.usecase.GetDeviceSerialUseCase +import io.github.openflocon.domain.adb.usecase.SendCommandUseCase +import io.github.openflocon.domain.common.getOrNull +import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase +import io.github.openflocon.flocondesktop.device.models.BatteryUiState +import io.github.openflocon.flocondesktop.device.models.ContentUiState +import io.github.openflocon.flocondesktop.device.models.CpuItem +import io.github.openflocon.flocondesktop.device.models.CpuUiState +import io.github.openflocon.flocondesktop.device.models.DeviceUiState +import io.github.openflocon.flocondesktop.device.models.InfoUiState +import io.github.openflocon.flocondesktop.device.models.MemoryItem +import io.github.openflocon.flocondesktop.device.models.MemoryUiState +import io.github.openflocon.flocondesktop.device.models.PermissionItem +import io.github.openflocon.flocondesktop.device.models.PermissionUiState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal class DeviceViewModel( + val deviceId: String, + val sendCommandUseCase: SendCommandUseCase, + val deviceSerialUseCase: GetDeviceSerialUseCase, + val currentDeviceAppsUseCase: GetCurrentDeviceIdAndPackageNameUseCase +) : ViewModel() { + + private val contentState = MutableStateFlow(ContentUiState(selectedTab = DeviceTab.entries.first())) + private val infoState = MutableStateFlow( + InfoUiState( + model = "", + brand = "", + versionRelease = "", + versionSdk = "", + serialNumber = "", + battery = "" + ) + ) + private val batteryState = MutableStateFlow( + BatteryUiState( + acPowered = false, + usbPowered = false, + wirelessPowered = false, + dockPowered = false, + maxChargingCurrent = 0, + maxChargingVoltage = 0, + chargeCounter = 0, + status = 0, + health = 0, + present = false, + level = 0, + scale = 0, + voltage = 0, + temperature = 0, + technology = "", + chargingState = 0, + chargingPolicy = 0, + capacityLevel = 0 + ) + ) + private val memoryState = MutableStateFlow(MemoryUiState(emptyList())) + private val cpuState = MutableStateFlow(CpuUiState(emptyList())) + private val permissionState = MutableStateFlow(PermissionUiState(emptyList())) + + val uiState = combine( + contentState, + infoState, + memoryState, + cpuState, + permissionState, + batteryState + ) { states -> + DeviceUiState( + contentState = states[0] as ContentUiState, + infoState = states[1] as InfoUiState, + memoryState = states[2] as MemoryUiState, + cpuState = states[3] as CpuUiState, + permissionState = states[4] as PermissionUiState, + batteryState = states[5] as BatteryUiState + ) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = DeviceUiState( + contentState = contentState.value, + infoState = infoState.value, + memoryState = memoryState.value, + cpuState = cpuState.value, + permissionState = permissionState.value, + batteryState = batteryState.value + ) + ) + + private var deviceSerial: String = "" + + init { + viewModelScope.launch(Dispatchers.IO) { + deviceSerial = deviceSerialUseCase(deviceId) + + onRefresh() + } + } + + fun onAction(action: DeviceAction) { + when (action) { + is DeviceAction.SelectTab -> onSelect(action) + DeviceAction.Refresh -> onRefresh() + is DeviceAction.ChangePermission -> onChangePermission(action) + } + } + + private fun onChangePermission(action: DeviceAction.ChangePermission) { + viewModelScope.launch { + if (action.granted) { + revokePermission(action.permission) + } else { + grantPermission(action.permission) + } + fetchPermission() + } + } + + private fun onSelect(action: DeviceAction.SelectTab) { + contentState.update { it.copy(selectedTab = action.selected) } + } + + private fun onRefresh() { + refreshCpu() + refreshMemory() + refreshBattery() + deviceInfo() + fetchPermission() + viewModelScope.launch { + // battery = sendCommand("shell", "dumpsys", "battery"), +// mem = sendCommand("shell", "dumpsys", "meminfo") + } + } + + private fun deviceInfo() { + viewModelScope.launch { + infoState.update { state -> + state.copy( + model = sendCommand("shell", "getprop", "ro.product.model"), + brand = sendCommand("shell", "getprop", "ro.product.brand"), + versionRelease = sendCommand("shell", "getprop", "ro.build.version.release"), + versionSdk = sendCommand("shell", "getprop", "ro.build.version.sdk"), + serialNumber = sendCommand("shell", "getprop", "ro.serialno") + ) + } + } + } + + private fun fetchPermission() { + viewModelScope.launch { + val packageName = currentDeviceAppsUseCase()?.packageName ?: return@launch + val command = sendCommand("shell", "dumpsys", "package", packageName) + val permissions = command.lines() + .dropWhile { !it.contains("runtime permissions:") } + .drop(1) + .takeWhile { it.contains("granted=") } + .map { it.trim() } + .filter { it.startsWith(PERMISSION_PREFIX) } + .mapNotNull { line -> + val list = line.split(":") + + PermissionItem( + name = list.getOrNull(0)?.removePrefix(PERMISSION_PREFIX) ?: return@mapNotNull null, + granted = list.getOrNull(1)?.contains("granted=true") ?: return@mapNotNull null, + ) + } + .sortedBy(PermissionItem::name) + + permissionState.update { it.copy(list = permissions) } + } + } + + private suspend fun grantPermission(permission: String) { + val packageName = currentDeviceAppsUseCase() ?: return + + sendCommand("shell", "pm", "grant", packageName.packageName, "${PERMISSION_PREFIX}$permission") + } + + private suspend fun revokePermission(permission: String) { + val packageName = currentDeviceAppsUseCase() ?: return + + sendCommand("shell", "pm", "revoke", packageName.packageName, "$PERMISSION_PREFIX$permission") + } + + private suspend fun sendCommand(vararg args: String): String { + return sendCommandUseCase(deviceSerial, *args) + .getOrNull() + .orEmpty() + .removeSuffix("\n") + } + + private fun refreshCpu() { + viewModelScope.launch(Dispatchers.IO) { + val output = sendCommand("shell", "dumpsys", "cpuinfo") + val regex = CPU_REGEX.toRegex() + val items = output.lineSequence() + .mapNotNull { regex.find(it) } + .mapNotNull { + try { + val packageName = it.groupValues[2].split("/") + + CpuItem( + cpuUsage = it.groupValues[1].toDoubleOrNull() ?: return@mapNotNull null, + packageName = packageName[1], + pId = packageName[0].toIntOrNull() ?: return@mapNotNull null, + userPercentage = it.groupValues[3].toDoubleOrNull() ?: return@mapNotNull null, + kernelPercentage = it.groupValues[4].toDoubleOrNull() ?: return@mapNotNull null, + minorFaults = it.groupValues[5].toIntOrNull(), + majorFaults = it.groupValues[6].toIntOrNull() + ) + } catch (e: NumberFormatException) { + // Handle parsing errors gracefully (e.g., log the error) + null + } + } + .sortedByDescending(CpuItem::cpuUsage) + .distinctBy(CpuItem::packageName) + .toList() + + cpuState.update { it.copy(list = items) } + } + } + + private fun refreshMemory() { + viewModelScope.launch(Dispatchers.IO) { + val output = sendCommand("shell", "dumpsys", "meminfo") + val regex = MEM_REGEX.toRegex() + val items = output.lineSequence() + .map { it.trim() } + .mapNotNull { regex.find(it) } + .mapNotNull { + try { + MemoryItem( + memoryUsage = formatMemoryUsage( + memoryUsageKB = it.groupValues[1].replace(",", "").toDoubleOrNull() + ), + processName = it.groupValues[2], + pid = it.groupValues[3].toIntOrNull() ?: return@mapNotNull null + ) + } catch (e: NumberFormatException) { + // Handle parsing errors gracefully (e.g., log the error) + null + } + } + .toList() + + memoryState.update { it.copy(list = items) } + } + } + + private fun formatMemoryUsage(memoryUsageKB: Double?): String { + if (memoryUsageKB == null) { + return "N/A" // Or handle null case as appropriate + } + + val memoryUsage = memoryUsageKB * 1024 // Convert KB to bytes + + return when { + memoryUsage < 1024 -> String.format("%.2f B", memoryUsage) + memoryUsage < 1024 * 1024 -> String.format("%.2f KB", memoryUsage / 1024) + memoryUsage < 1024 * 1024 * 1024 -> String.format("%.2f MB", memoryUsage / (1024 * 1024)) + else -> String.format("%.2f GB", memoryUsage / (1024 * 1024 * 1024)) + } + } + + private fun refreshBattery() { + viewModelScope.launch { + val batteryInfo = sendCommand("shell", "dumpsys", "battery") + + batteryState.update { + it.copy( + acPowered = AC_POWERED_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toBoolean() ?: false, + usbPowered = USB_POWERED_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toBoolean() ?: false, + wirelessPowered = WIRELESS_POWERED_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toBoolean() + ?: false, + dockPowered = DOCK_POWERED_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toBoolean() ?: false, + maxChargingCurrent = MAX_CHARGING_CURRENT_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(), + maxChargingVoltage = MAX_CHARGING_VOLTAGE_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(), + chargeCounter = CHARGE_COUNTER_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(), + status = STATUS_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(), + health = HEALTH_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(), + present = PRESENT_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toBoolean() ?: false, + level = LEVEL_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(), + scale = SCALE_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(), + voltage = VOLTAGE_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(), + temperature = TEMPERATURE_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(), + technology = TECHNOLOGY_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1), + chargingState = CHARGING_STATE_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(), + chargingPolicy = CHARGING_POLICY_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(), + capacityLevel = CAPACITY_LEVEL_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull() + ) + } + } + } + + companion object { + private const val PERMISSION_PREFIX = "android.permission." + + // CPU + private const val CPU_REGEX = + """(\d+(?:\.\d+)?)%\s+([^:]+):\s+(\d+(?:\.\d+)?)%\s+user\s+\+\s+(\d+(?:\.\d+)?)%\s+kernel\s+/ faults:\s+(\d+)\s+minor\s+(\d+)\s+major""" + + // MEM + private const val MEM_REGEX = """([\d,]+)K:\s+([a-zA-Z0-9._:-]+)\s+$${"pid"}\s+(\d+)(?:\s+/\s+([a-zA-Z\s]+))?$""" + + // Battery + private const val AC_POWERED_REGEX = """AC powered:\s+(true|false)""" + private const val USB_POWERED_REGEX = """USB powered:\s+(true|false)""" + private const val WIRELESS_POWERED_REGEX = """Wireless powered:\s+(true|false)""" + private const val DOCK_POWERED_REGEX = """Dock powered:\s+(true|false)""" + private const val MAX_CHARGING_CURRENT_REGEX = """Max charging current:\s+(\d+)""" + private const val MAX_CHARGING_VOLTAGE_REGEX = """Max charging voltage:\s+(\d+)""" + private const val CHARGE_COUNTER_REGEX = """Charge counter:\s+(\d+)""" + private const val STATUS_REGEX = """status:\s+(\d+)""" + private const val HEALTH_REGEX = """health:\s+(\d+)""" + private const val PRESENT_REGEX = """present:\s+(true|false)""" + private const val LEVEL_REGEX = """level:\s+(\d+)""" + private const val SCALE_REGEX = """scale:\s+(\d+)""" + private const val VOLTAGE_REGEX = """voltage:\s+(\d+)""" + private const val TEMPERATURE_REGEX = """temperature:\s+(\d+)""" + private const val TECHNOLOGY_REGEX = """technology:\s+([a-zA-Z-]+)""" + private const val CHARGING_STATE_REGEX = """Charging state:\s+(\d+)""" + private const val CHARGING_POLICY_REGEX = """Charging policy:\s+(\d+)""" + private const val CAPACITY_LEVEL_REGEX = """Capacity level:\s+(\d+)""" + } + +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/BatteryUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/BatteryUiState.kt new file mode 100644 index 000000000..1a0076642 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/BatteryUiState.kt @@ -0,0 +1,46 @@ +package io.github.openflocon.flocondesktop.device.models + +import androidx.compose.runtime.Immutable + +@Immutable +data class BatteryUiState( + val acPowered: Boolean?, + val usbPowered: Boolean?, + val wirelessPowered: Boolean?, + val dockPowered: Boolean?, + val maxChargingCurrent: Int?, + val maxChargingVoltage: Int?, + val chargeCounter: Int?, + val status: Int?, + val health: Int?, + val present: Boolean?, + val level: Int?, + val scale: Int?, + val voltage: Int?, + val temperature: Int?, + val technology: String?, + val chargingState: Int?, + val chargingPolicy: Int?, + val capacityLevel: Int? +) + +fun previewBatteryUiState() = BatteryUiState( + acPowered = true, + usbPowered = false, + wirelessPowered = true, + dockPowered = false, + maxChargingCurrent = 100, + maxChargingVoltage = 100, + chargeCounter = 100, + status = 100, + health = 100, + present = true, + level = 100, + scale = 100, + voltage = 100, + temperature = 100, + technology = "Technology", + chargingState = 100, + chargingPolicy = 100, + capacityLevel = 100 +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/ContentUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/ContentUiState.kt new file mode 100644 index 000000000..32b26ab94 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/ContentUiState.kt @@ -0,0 +1,13 @@ +package io.github.openflocon.flocondesktop.device.models + +import androidx.compose.runtime.Immutable +import io.github.openflocon.flocondesktop.device.DeviceTab + +@Immutable +data class ContentUiState( + val selectedTab: DeviceTab +) + +fun previewContentUiState() = ContentUiState( + selectedTab = DeviceTab.INFORMATION +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/CpuUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/CpuUiState.kt new file mode 100644 index 000000000..5a7fecad9 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/CpuUiState.kt @@ -0,0 +1,23 @@ +package io.github.openflocon.flocondesktop.device.models + +import androidx.compose.runtime.Immutable + +@Immutable +data class CpuUiState( + val list: List +) + +@Immutable +data class CpuItem( + val cpuUsage: Double, + val pId: Int, + val packageName: String, + val userPercentage: Double, + val kernelPercentage: Double, + val minorFaults: Int?, + val majorFaults: Int? +) + +fun previewCpuUiState() = CpuUiState( + list = emptyList() +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/DeviceUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/DeviceUiState.kt new file mode 100644 index 000000000..d1fa53445 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/DeviceUiState.kt @@ -0,0 +1,22 @@ +package io.github.openflocon.flocondesktop.device.models + +import androidx.compose.runtime.Immutable + +@Immutable +data class DeviceUiState( + val contentState: ContentUiState, + val infoState: InfoUiState, + val cpuState: CpuUiState, + val memoryState: MemoryUiState, + val permissionState: PermissionUiState, + val batteryState: BatteryUiState +) + +internal fun previewDeviceUiState() = DeviceUiState( + contentState = previewContentUiState(), + cpuState = previewCpuUiState(), + memoryState = previewMemoryUiState(), + infoState = previewInfoUiState(), + permissionState = previewPermissionUiState(), + batteryState = previewBatteryUiState() +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/InfoUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/InfoUiState.kt new file mode 100644 index 000000000..72c80782b --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/InfoUiState.kt @@ -0,0 +1,22 @@ +package io.github.openflocon.flocondesktop.device.models + +import androidx.compose.runtime.Immutable + +@Immutable +data class InfoUiState( + val model: String, + val brand: String, + val versionRelease: String, + val versionSdk: String, + val serialNumber: String, + val battery: String +) + +fun previewInfoUiState() = InfoUiState( + model = "", + brand = "", + versionRelease = "", + versionSdk = "", + serialNumber = "", + battery = "" +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/MemoryUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/MemoryUiState.kt new file mode 100644 index 000000000..8678bc166 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/MemoryUiState.kt @@ -0,0 +1,19 @@ +package io.github.openflocon.flocondesktop.device.models + +import androidx.compose.runtime.Immutable + +@Immutable +data class MemoryUiState( + val list: List +) + +@Immutable +data class MemoryItem( + val memoryUsage: String, + val processName: String, + val pid: Int +) + +fun previewMemoryUiState() = MemoryUiState( + list = emptyList() +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/PermissionUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/PermissionUiState.kt new file mode 100644 index 000000000..b11964944 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/PermissionUiState.kt @@ -0,0 +1,18 @@ +package io.github.openflocon.flocondesktop.device.models + +import androidx.compose.runtime.Immutable + +@Immutable +data class PermissionUiState( + val list: List +) + +@Immutable +data class PermissionItem( + val name: String, + val granted: Boolean +) + +fun previewPermissionUiState() = PermissionUiState( + list = emptyList() +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/BatteryPage.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/BatteryPage.kt new file mode 100644 index 000000000..928baec33 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/BatteryPage.kt @@ -0,0 +1,93 @@ +package io.github.openflocon.flocondesktop.device.pages + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import io.github.openflocon.flocondesktop.device.models.BatteryUiState +import io.github.openflocon.library.designsystem.FloconTheme +import io.github.openflocon.library.designsystem.components.FloconTextValue + +@Composable +internal fun BatteryPage( + state: BatteryUiState +) { + Column( + modifier = Modifier + .fillMaxSize() + .clip(FloconTheme.shapes.medium) + .background(FloconTheme.colorPalette.primary) + .border( + width = 1.dp, + color = FloconTheme.colorPalette.secondary, + shape = FloconTheme.shapes.medium + ) + .verticalScroll(rememberScrollState()) + .padding(8.dp) + ) { + FloconTextValue(label = "Technology", value = state.technology.orEmpty(), valueContainerColor = FloconTheme.colorPalette.secondary) + FloconTextValue(label = "Health", value = state.health?.toString().orEmpty(), valueContainerColor = FloconTheme.colorPalette.secondary) + FloconTextValue( + label = "Capacity", + value = state.capacityLevel?.toString().orEmpty(), + valueContainerColor = FloconTheme.colorPalette.secondary + ) + FloconTextValue(label = "AC Powered", value = state.acPowered?.toString().orEmpty(), valueContainerColor = FloconTheme.colorPalette.secondary) + FloconTextValue( + label = "Charge counter", + value = state.chargeCounter?.toString().orEmpty(), + valueContainerColor = FloconTheme.colorPalette.secondary + ) + FloconTextValue( + label = "Charging policy", + value = state.chargingPolicy?.toString().orEmpty(), + valueContainerColor = FloconTheme.colorPalette.secondary + ) + FloconTextValue( + label = "Charging state", + value = state.chargingState?.toString().orEmpty(), + valueContainerColor = FloconTheme.colorPalette.secondary + ) + FloconTextValue( + label = "Max Charging current", + value = state.maxChargingCurrent?.toString().orEmpty(), + valueContainerColor = FloconTheme.colorPalette.secondary + ) + FloconTextValue( + label = "Max Charging voltage", + value = state.maxChargingVoltage?.toString().orEmpty(), + valueContainerColor = FloconTheme.colorPalette.secondary + ) + FloconTextValue( + label = "Dock powered", + value = state.dockPowered?.toString().orEmpty(), + valueContainerColor = FloconTheme.colorPalette.secondary + ) + FloconTextValue(label = "Level", value = state.level?.toString().orEmpty(), valueContainerColor = FloconTheme.colorPalette.secondary) + FloconTextValue(label = "Voltage", value = state.voltage?.toString().orEmpty(), valueContainerColor = FloconTheme.colorPalette.secondary) + FloconTextValue( + label = "Temperature", + value = state.temperature?.toString().orEmpty(), + valueContainerColor = FloconTheme.colorPalette.secondary + ) + FloconTextValue( + label = "Wireless powered", + value = state.wirelessPowered?.toString().orEmpty(), + valueContainerColor = FloconTheme.colorPalette.secondary + ) + FloconTextValue(label = "Scale", value = state.scale?.toString().orEmpty(), valueContainerColor = FloconTheme.colorPalette.secondary) + FloconTextValue( + label = "USB Powered", + value = state.usbPowered?.toString().orEmpty(), + valueContainerColor = FloconTheme.colorPalette.secondary + ) + FloconTextValue(label = "Present", value = state.present?.toString().orEmpty(), valueContainerColor = FloconTheme.colorPalette.secondary) + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/CpuPage.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/CpuPage.kt new file mode 100644 index 000000000..3112b2d0c --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/CpuPage.kt @@ -0,0 +1,141 @@ +package io.github.openflocon.flocondesktop.device.pages + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.github.openflocon.flocondesktop.device.DeviceAction +import io.github.openflocon.flocondesktop.device.models.CpuUiState +import io.github.openflocon.library.designsystem.FloconTheme +import io.github.openflocon.library.designsystem.components.FloconHorizontalDivider + +@Composable +internal fun CpuPage( + state: CpuUiState, + onAction: (DeviceAction) -> Unit +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .clip(FloconTheme.shapes.medium) + .background(FloconTheme.colorPalette.primary) + .border( + width = 1.dp, + color = FloconTheme.colorPalette.secondary, + shape = FloconTheme.shapes.medium + ) + ) { + stickyHeader { + BasicItem( + cpuUsage = "Usage (%)", + userPercentage = "User (%)", + kernelPercentage = "Kernel (%)", + packageName = "Package Name", + pId = "pId", + majorFaults = "Major Faults", + minorFaults = "Minor Faults", + style = FloconTheme.typography.labelSmall + .copy(fontWeight = FontWeight.Bold) + ) + FloconHorizontalDivider( + color = FloconTheme.colorPalette.secondary + ) + } + itemsIndexed( + items = state.list, + key = { _, item -> item.pId } + ) { index, item -> + BasicItem( + cpuUsage = item.cpuUsage.toString(), + userPercentage = item.userPercentage.toString(), + kernelPercentage = item.kernelPercentage.toString(), + pId = item.pId.toString(), + packageName = item.packageName, + majorFaults = item.majorFaults?.toString().orEmpty(), + minorFaults = item.minorFaults?.toString().orEmpty(), + style = FloconTheme.typography.labelSmall + ) + if (index != state.list.lastIndex) { + FloconHorizontalDivider( + color = FloconTheme.colorPalette.secondary + ) + } + } + } +} + +@Composable +private fun BasicItem( + cpuUsage: String, + userPercentage: String, + kernelPercentage: String, + pId: String, + packageName: String, + majorFaults: String, + minorFaults: String, + style: TextStyle +) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .background(FloconTheme.colorPalette.primary) + .padding(4.dp) + ) { + Text( + text = cpuUsage, + style = style, + textAlign = TextAlign.Center, + modifier = Modifier.weight(.2f) + ) + Text( + text = userPercentage, + style = style, + textAlign = TextAlign.Center, + modifier = Modifier.weight(.2f) + ) + Text( + text = kernelPercentage, + style = style, + textAlign = TextAlign.Center, + modifier = Modifier.weight(.2f) + ) + Text( + text = pId, + style = style, + modifier = Modifier.weight(.2f) + ) + Text( + text = packageName, + style = style, + modifier = Modifier.weight(1f) + ) + Text( + text = majorFaults, + style = style, + textAlign = TextAlign.Center, + modifier = Modifier.weight(.2f) + ) + Text( + text = minorFaults, + style = style, + textAlign = TextAlign.Center, + modifier = Modifier.weight(.2f) + ) + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/InfoPage.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/InfoPage.kt new file mode 100644 index 000000000..0c37c2655 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/InfoPage.kt @@ -0,0 +1,100 @@ +package io.github.openflocon.flocondesktop.device.pages + +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.ColumnScope +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.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import io.github.openflocon.flocondesktop.device.models.InfoUiState +import io.github.openflocon.library.designsystem.FloconTheme +import io.github.openflocon.library.designsystem.components.FloconSection +import io.github.openflocon.library.designsystem.components.FloconTextValue + +@Composable +internal fun InfoPage( + state: InfoUiState +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + General(state) + } +} + +@Composable +private fun General( + state: InfoUiState +) { + Section( + title = "General" + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(8.dp) + ) { + FloconTextValue( + label = "Brand", + value = state.brand, + valueContainerColor = FloconTheme.colorPalette.secondary + ) + FloconTextValue( + label = "Battery", + value = state.battery, + valueContainerColor = FloconTheme.colorPalette.secondary + ) + FloconTextValue( + label = "Serial number", + value = state.serialNumber, + valueContainerColor = FloconTheme.colorPalette.secondary + ) + FloconTextValue( + label = "Version - Release", + value = state.versionRelease, + valueContainerColor = FloconTheme.colorPalette.secondary + ) + FloconTextValue( + label = "Version - Sdk", + value = state.versionSdk, + valueContainerColor = FloconTheme.colorPalette.secondary + ) + } + } +} + +@Composable +private fun Section( + title: String, + content: @Composable ColumnScope.() -> Unit +) { + FloconSection( + title = title, + initialValue = true, + modifier = Modifier + .fillMaxWidth() + .clip(FloconTheme.shapes.medium) + .background(FloconTheme.colorPalette.primary) + .border( + width = 1.dp, + color = FloconTheme.colorPalette.secondary, + shape = FloconTheme.shapes.medium + ) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(8.dp), + content = content + ) + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/MemoryPage.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/MemoryPage.kt new file mode 100644 index 000000000..913b68c82 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/MemoryPage.kt @@ -0,0 +1,100 @@ +package io.github.openflocon.flocondesktop.device.pages + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.github.openflocon.flocondesktop.device.models.MemoryUiState +import io.github.openflocon.library.designsystem.FloconTheme +import io.github.openflocon.library.designsystem.components.FloconHorizontalDivider + +@Composable +internal fun MemoryPage( + state: MemoryUiState +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .clip(FloconTheme.shapes.medium) + .background(FloconTheme.colorPalette.primary) + .border( + width = 1.dp, + color = FloconTheme.colorPalette.secondary, + shape = FloconTheme.shapes.medium + ) + ) { + stickyHeader { + BasicItem( + memoryUsage = "Usage", + processName = "Package name", + pId = "pId", + style = FloconTheme.typography.labelSmall.copy(fontWeight = FontWeight.Bold) + ) + } + itemsIndexed( + items = state.list, + key = { _, item -> item.pid } + ) { index, item -> + BasicItem( + memoryUsage = item.memoryUsage, + pId = item.pid.toString(), + processName = item.processName, + style = FloconTheme.typography.labelSmall + ) + if (index != state.list.lastIndex) { + FloconHorizontalDivider( + color = FloconTheme.colorPalette.secondary + ) + } + } + } +} + +@Composable +private fun BasicItem( + memoryUsage: String, + processName: String, + pId: String, + style: TextStyle +) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .background(FloconTheme.colorPalette.primary) + .padding(4.dp) + ) { + Text( + text = memoryUsage, + style = style, + textAlign = TextAlign.Center, + modifier = Modifier.weight(.2f) + ) + Text( + text = pId, + style = style, + textAlign = TextAlign.Center, + modifier = Modifier.weight(.2f) + ) + Text( + text = processName, + style = style, + modifier = Modifier.weight(1f) + ) + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/PermissionPage.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/PermissionPage.kt new file mode 100644 index 000000000..facb77b12 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/pages/PermissionPage.kt @@ -0,0 +1,79 @@ +package io.github.openflocon.flocondesktop.device.pages + +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.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import io.github.openflocon.flocondesktop.device.DeviceAction +import io.github.openflocon.flocondesktop.device.models.PermissionUiState +import io.github.openflocon.library.designsystem.FloconTheme +import io.github.openflocon.library.designsystem.components.FloconCheckbox +import io.github.openflocon.library.designsystem.components.FloconHorizontalDivider + +@Composable +internal fun PermissionPage( + state: PermissionUiState, + onAction: (DeviceAction) -> Unit +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .clip(FloconTheme.shapes.medium) + .background(FloconTheme.colorPalette.primary) + .border( + width = 1.dp, + color = FloconTheme.colorPalette.secondary, + shape = FloconTheme.shapes.medium + ) + ) { + itemsIndexed( + items = state.list, + key = { _, item -> item.name } + ) { index, item -> + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clip(FloconTheme.shapes.medium) + .clickable(onClick = { + onAction( + DeviceAction.ChangePermission( + permission = item.name, + granted = item.granted + ) + ) + }) + .padding(vertical = 4.dp, horizontal = 8.dp) + ) { + Text( + text = item.name, + style = FloconTheme.typography.labelSmall, + modifier = Modifier.weight(1f) + ) + FloconCheckbox( + checked = item.granted, + onCheckedChange = null, + uncheckedColor = FloconTheme.colorPalette.secondary + ) + } + if (index != state.list.lastIndex) { + FloconHorizontalDivider( + color = FloconTheme.colorPalette.secondary + ) + } + } + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/view/NetworkScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/view/NetworkScreen.kt index f39d49355..e16f9fbed 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/view/NetworkScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/view/NetworkScreen.kt @@ -297,7 +297,8 @@ fun NetworkScreen( deletedJson.forEach { states.remove(it) } addedJson.forEach { states.put( - it, createFloconWindowState(), + it, + createFloconWindowState(), ) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/settings/SettingsScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/settings/SettingsScreen.kt index 33c12358c..6614dddc6 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/settings/SettingsScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/settings/SettingsScreen.kt @@ -23,6 +23,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import co.touchlab.kermit.Logger +import io.github.openflocon.flocondesktop.common.ui.window.FloconWindow +import io.github.openflocon.flocondesktop.common.ui.window.createFloconWindowState import io.github.openflocon.library.designsystem.FloconTheme import io.github.openflocon.library.designsystem.components.FloconButton import io.github.openflocon.library.designsystem.components.FloconFeature diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/device/TopBarDeviceView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/device/TopBarDeviceView.kt index 0a0f5c2ca..412412ddd 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/device/TopBarDeviceView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/device/TopBarDeviceView.kt @@ -12,13 +12,18 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MobileOff import androidx.compose.material.icons.filled.Smartphone import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Details import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +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.clip @@ -28,9 +33,11 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import io.github.openflocon.flocondesktop.device.DeviceScreen import io.github.openflocon.flocondesktop.main.ui.model.DeviceItemUiModel import io.github.openflocon.library.designsystem.FloconTheme import io.github.openflocon.library.designsystem.components.FloconIcon +import io.github.openflocon.library.designsystem.components.FloconIconButton @Composable internal fun TopBarDeviceView( @@ -40,6 +47,8 @@ internal fun TopBarDeviceView( selected: Boolean = false, onDelete: (() -> Unit)? = null, ) { + var openDetail by remember { mutableStateOf(false) } + Row( modifier = modifier .then( @@ -94,15 +103,21 @@ internal fun TopBarDeviceView( ), ) } + FloconIconButton( + onClick = { openDetail = true } + ) { + FloconIcon( + imageVector = Icons.Outlined.Details + ) + } if (!selected && onDelete != null) { Spacer(modifier = Modifier.weight(1f)) Box( - Modifier.clip(RoundedCornerShape(4.dp)) - .background( - Color.White.copy(alpha = 0.8f) - ).padding(2.dp).clickable { - onDelete() - }, + Modifier + .clip(FloconTheme.shapes.small) + .background(Color.White.copy(alpha = 0.8f)) + .padding(2.dp) + .clickable(onClick = onDelete), contentAlignment = Alignment.Center, ) { FloconIcon( @@ -114,4 +129,13 @@ internal fun TopBarDeviceView( } } } + + if (openDetail) { + key(device.id) { + DeviceScreen( + deviceId = device.id, + onCloseRequest = { openDetail = false } + ) + } + } } diff --git a/FloconDesktop/composeApp/src/desktopMain/java/io/github/openflocon/flocondesktop/common/ui/window/FloconWindow.desktop.kt b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/common/ui/window/FloconWindow.desktop.kt similarity index 81% rename from FloconDesktop/composeApp/src/desktopMain/java/io/github/openflocon/flocondesktop/common/ui/window/FloconWindow.desktop.kt rename to FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/common/ui/window/FloconWindow.desktop.kt index 553a65a61..ab906544f 100644 --- a/FloconDesktop/composeApp/src/desktopMain/java/io/github/openflocon/flocondesktop/common/ui/window/FloconWindow.desktop.kt +++ b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/common/ui/window/FloconWindow.desktop.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.window.FrameWindowScope import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowPosition @@ -17,6 +18,19 @@ import flocondesktop.composeapp.generated.resources.app_icon import io.github.openflocon.library.designsystem.components.escape.LocalEscapeHandlerStack import org.jetbrains.compose.resources.painterResource +//actual fun rememberFloconWindowState( +// placement: WindowPlacement, +// position: WindowPosition, +// size: DpSize +//): FloconWindowState { +// return FloconWindowStateDesktop( +// WindowState( +// placement = WindowPlacement.Floating, +// position = WindowPosition(Alignment.Center), +// ) +// ) +//} + data class FloconWindowStateDesktop( val windowState: WindowState, ) : FloconWindowState @@ -38,7 +52,7 @@ actual fun FloconWindow( state: FloconWindowState, onCloseRequest: () -> Unit, alwaysOnTop: Boolean, - content: @Composable () -> Unit, + content: @Composable FrameWindowScope.() -> Unit, ) { val handlers = remember { mutableStateListOf<() -> Boolean>() } diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/DI.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/DI.kt index de5a70b3f..49dd35893 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/DI.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/DI.kt @@ -1,8 +1,13 @@ package io.github.openflocon.domain.adb +import io.github.openflocon.domain.adb.usecase.ExecuteAdbCommandUseCase +import io.github.openflocon.domain.adb.usecase.GetDeviceSerialUseCase +import io.github.openflocon.domain.adb.usecase.SendCommandUseCase import org.koin.core.module.dsl.factoryOf import org.koin.dsl.module internal val adbModule = module { factoryOf(::ExecuteAdbCommandUseCase) + factoryOf(::SendCommandUseCase) + factoryOf(::GetDeviceSerialUseCase) } diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/ExecuteAdbCommandUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/usecase/ExecuteAdbCommandUseCase.kt similarity index 95% rename from FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/ExecuteAdbCommandUseCase.kt rename to FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/usecase/ExecuteAdbCommandUseCase.kt index 24bd3840b..0ed68047f 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/ExecuteAdbCommandUseCase.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/usecase/ExecuteAdbCommandUseCase.kt @@ -1,4 +1,4 @@ -package io.github.openflocon.domain.adb +package io.github.openflocon.domain.adb.usecase import io.github.openflocon.domain.adb.model.AdbCommandTargetDomainModel import io.github.openflocon.domain.adb.repository.AdbRepository diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/usecase/GetDeviceSerialUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/usecase/GetDeviceSerialUseCase.kt new file mode 100644 index 000000000..ca844effd --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/usecase/GetDeviceSerialUseCase.kt @@ -0,0 +1,13 @@ +package io.github.openflocon.domain.adb.usecase + +import io.github.openflocon.domain.adb.repository.AdbRepository + +class GetDeviceSerialUseCase internal constructor( + private val adbRepository: AdbRepository +) { + suspend operator fun invoke( + deviceId: String + ): String { + return adbRepository.getAdbSerial(deviceId).orEmpty() + } +} diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/usecase/SendCommandUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/usecase/SendCommandUseCase.kt new file mode 100644 index 000000000..9845b412a --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/adb/usecase/SendCommandUseCase.kt @@ -0,0 +1,24 @@ +package io.github.openflocon.domain.adb.usecase + +import io.github.openflocon.domain.adb.repository.AdbRepository +import io.github.openflocon.domain.common.Either +import io.github.openflocon.domain.settings.repository.SettingsRepository +import kotlinx.coroutines.flow.firstOrNull + +class SendCommandUseCase( + private val adbRepository: AdbRepository, + private val settingsRepository: SettingsRepository +) { + + suspend operator fun invoke( + deviceSerial: String, + vararg args: String + ): Either = Either.catch { + return adbRepository.executeAdbCommand( + adbPath = settingsRepository.adbPath.firstOrNull() ?: throw Throwable("error"), + command = args.joinToString(separator = " "), + deviceSerial = deviceSerial + ) + } + +} diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/usecase/ExecuteDeeplinkUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/usecase/ExecuteDeeplinkUseCase.kt index f192b354e..c1b18608b 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/usecase/ExecuteDeeplinkUseCase.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/usecase/ExecuteDeeplinkUseCase.kt @@ -1,6 +1,6 @@ package io.github.openflocon.domain.deeplink.usecase -import io.github.openflocon.domain.adb.ExecuteAdbCommandUseCase +import io.github.openflocon.domain.adb.usecase.ExecuteAdbCommandUseCase import io.github.openflocon.domain.adb.model.AdbCommandTargetDomainModel import io.github.openflocon.domain.deeplink.models.DeeplinkDomainModel import io.github.openflocon.domain.deeplink.repository.DeeplinkRepository diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/device/usecase/StartRecordingVideoUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/device/usecase/StartRecordingVideoUseCase.kt index 6ef527cf3..c2a0217cb 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/device/usecase/StartRecordingVideoUseCase.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/device/usecase/StartRecordingVideoUseCase.kt @@ -1,6 +1,6 @@ package io.github.openflocon.domain.device.usecase -import io.github.openflocon.domain.adb.ExecuteAdbCommandUseCase +import io.github.openflocon.domain.adb.usecase.ExecuteAdbCommandUseCase import io.github.openflocon.domain.adb.model.AdbCommandTargetDomainModel import io.github.openflocon.domain.common.Either import io.github.openflocon.domain.common.Failure diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/device/usecase/StopRecordingVideoUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/device/usecase/StopRecordingVideoUseCase.kt index a9f78be55..542171dbd 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/device/usecase/StopRecordingVideoUseCase.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/device/usecase/StopRecordingVideoUseCase.kt @@ -1,6 +1,6 @@ package io.github.openflocon.domain.device.usecase -import io.github.openflocon.domain.adb.ExecuteAdbCommandUseCase +import io.github.openflocon.domain.adb.usecase.ExecuteAdbCommandUseCase import io.github.openflocon.domain.adb.model.AdbCommandTargetDomainModel import io.github.openflocon.domain.common.Either import io.github.openflocon.domain.common.Failure diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/device/usecase/TakeScreenshotUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/device/usecase/TakeScreenshotUseCase.kt index 343af695d..b05587263 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/device/usecase/TakeScreenshotUseCase.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/device/usecase/TakeScreenshotUseCase.kt @@ -1,6 +1,6 @@ package io.github.openflocon.domain.device.usecase -import io.github.openflocon.domain.adb.ExecuteAdbCommandUseCase +import io.github.openflocon.domain.adb.usecase.ExecuteAdbCommandUseCase import io.github.openflocon.domain.adb.model.AdbCommandTargetDomainModel import io.github.openflocon.domain.common.Either import io.github.openflocon.domain.common.Failure diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/settings/usecase/StartAdbForwardUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/settings/usecase/StartAdbForwardUseCase.kt index 1f088a5b1..0d733a9c2 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/settings/usecase/StartAdbForwardUseCase.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/settings/usecase/StartAdbForwardUseCase.kt @@ -1,7 +1,7 @@ package io.github.openflocon.domain.settings.usecase import io.github.openflocon.domain.Constant -import io.github.openflocon.domain.adb.ExecuteAdbCommandUseCase +import io.github.openflocon.domain.adb.usecase.ExecuteAdbCommandUseCase import io.github.openflocon.domain.adb.model.AdbCommandTargetDomainModel import io.github.openflocon.domain.common.Either diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/settings/usecase/TestAdbUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/settings/usecase/TestAdbUseCase.kt index 060b7a4b3..faa4a3a60 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/settings/usecase/TestAdbUseCase.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/settings/usecase/TestAdbUseCase.kt @@ -1,6 +1,6 @@ package io.github.openflocon.domain.settings.usecase -import io.github.openflocon.domain.adb.ExecuteAdbCommandUseCase +import io.github.openflocon.domain.adb.usecase.ExecuteAdbCommandUseCase import io.github.openflocon.domain.adb.model.AdbCommandTargetDomainModel import io.github.openflocon.domain.common.Either diff --git a/FloconDesktop/gradle/libs.versions.toml b/FloconDesktop/gradle/libs.versions.toml index 27cc66626..1017e1d3e 100644 --- a/FloconDesktop/gradle/libs.versions.toml +++ b/FloconDesktop/gradle/libs.versions.toml @@ -11,7 +11,7 @@ androidx-espresso = "3.7.0" androidx-lifecycle = "2.9.2" androidx-testExt = "1.3.0" coil = "3.3.0" -composeHotReload = "1.0.0-beta05" +composeHotReload = "1.0.0-beta08" composeMultiplatform = "1.9.0" junit = "4.13.2" kermit = "2.0.8" diff --git a/FloconDesktop/library/designsystem/build.gradle.kts b/FloconDesktop/library/designsystem/build.gradle.kts index 98a1843aa..41fcdb7bc 100644 --- a/FloconDesktop/library/designsystem/build.gradle.kts +++ b/FloconDesktop/library/designsystem/build.gradle.kts @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.composeCompiler) alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeHotReload) +// alias(libs.plugins.composeHotReload) } kotlin { diff --git a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconCheckbox.kt b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconCheckbox.kt index 5354a6ad9..72cb1c01e 100644 --- a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconCheckbox.kt +++ b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconCheckbox.kt @@ -10,7 +10,7 @@ import io.github.openflocon.library.designsystem.FloconTheme @Composable fun FloconCheckbox( checked: Boolean, - onCheckedChange: (Boolean) -> Unit, + onCheckedChange: ((Boolean) -> Unit)?, modifier: Modifier = Modifier, uncheckedColor: Color = FloconTheme.colorPalette.primary ) { diff --git a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconScrollableTabRow.kt b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconScrollableTabRow.kt new file mode 100644 index 000000000..a5d2c7646 --- /dev/null +++ b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconScrollableTabRow.kt @@ -0,0 +1,21 @@ +package io.github.openflocon.library.designsystem.components + +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun FloconScrollableTabRow( + selectedTabIndex: Int, + modifier: Modifier = Modifier, + tabs: @Composable () -> Unit +) { + ScrollableTabRow( + selectedTabIndex = selectedTabIndex, + modifier = modifier, + edgePadding = 0.dp, + divider = {}, + tabs = tabs + ) +} diff --git a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconTab.kt b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconTab.kt new file mode 100644 index 000000000..0e512fafe --- /dev/null +++ b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconTab.kt @@ -0,0 +1,53 @@ +package io.github.openflocon.library.designsystem.components + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun FloconTab( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + selectedContentColor: Color = LocalContentColor.current, + unselectedContentColor: Color = selectedContentColor, + content: @Composable ColumnScope.() -> Unit +) { + Tab( + selected = selected, + onClick = onClick, + content = content, + selectedContentColor = selectedContentColor, + unselectedContentColor = unselectedContentColor, + modifier = modifier + ) +} + +@Composable +fun FloconTab( + text: String, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + selectedContentColor: Color = LocalContentColor.current, + unselectedContentColor: Color = selectedContentColor, +) { + FloconTab( + selected = selected, + onClick = onClick, + selectedContentColor = selectedContentColor, + unselectedContentColor = unselectedContentColor, + modifier = modifier + ) { + Text( + text = text, + modifier = Modifier.padding(4.dp) + ) + } +} diff --git a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconTextValue.kt b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconTextValue.kt new file mode 100644 index 000000000..ea8300c98 --- /dev/null +++ b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconTextValue.kt @@ -0,0 +1,48 @@ +package io.github.openflocon.library.designsystem.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import io.github.openflocon.library.designsystem.FloconTheme +import io.github.openflocon.library.designsystem.theme.contentColorFor + +@Composable +fun FloconTextValue( + label: String, + value: String, + modifier: Modifier = Modifier, + valueContainerColor: Color = FloconTheme.colorPalette.primary +) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.padding(2.dp) + ) { + Text( + text = label, + style = FloconTheme.typography.labelSmall, + modifier = Modifier.weight(1f) + ) + SelectionContainer( + modifier = Modifier.weight(1f) + .clip(FloconTheme.shapes.small) + .background(valueContainerColor) + .padding(4.dp) + ) { + Text( + text = value, + style = FloconTheme.typography.labelSmall, + color = FloconTheme.colorPalette.contentColorFor(valueContainerColor) + ) + } + } +}