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)
+ )
+ }
+ }
+}