From 81087e533492d52929b1798c0546c83e9b99b8c0 Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Wed, 8 Jul 2026 09:57:49 +0200 Subject: [PATCH 1/6] feat: add logs tab to settings and improve ADB initialization reporting Introduces a centralized LogManager to track application events, exposed through a new "Logs" tab in the Settings screen. Additionally, updates the Settings UI to use a side-drawer layout and improves ADB process feedback by logging success and error states. --- .../app/ui/settings/SettingsScreen.kt | 70 +++++++++++++++++++ .../app/ui/settings/SettingsUiState.kt | 1 - .../app/ui/settings/SettingsViewModel.kt | 5 ++ FloconDesktop/gradle/libs.versions.toml | 2 +- 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt index d403fac7d..53114c62f 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt @@ -436,6 +436,11 @@ private fun AdbPane( style = FloconTheme.typography.bodySmall, modifier = Modifier.weight(1f) ) + Text( + text = stringResource(Res.string.settings_adb_valid), + color = FloconTheme.colorPalette.onAccent, + style = FloconTheme.typography.bodySmall, + ) } } } @@ -690,6 +695,71 @@ private fun AboutPane( // Shared helpers // --------------------------------------------------------------------------- +@Composable +private fun LogsPane( + logs: List, + onClearLogs: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier.fillMaxSize() + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Console (${logs.size})", + style = FloconTheme.typography.titleMedium, + color = FloconTheme.colorPalette.onPrimary, + ) + if (logs.isNotEmpty()) { + SettingsButton( + text = "Clear", + onClick = onClearLogs, + ) + } + } + + if (logs.isEmpty()) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + Text( + text = "No logs yet", + style = FloconTheme.typography.bodySmall, + color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.4f), + ) + } + } else { + ConsoleLogPanel( + logs = logs, + modifier = Modifier.fillMaxWidth().weight(1f), + ) + } + } +} + +@Composable +private fun AboutPane( + modifier: Modifier = Modifier, +) { + AboutScreen( + modifier = modifier + .fillMaxSize() + .background(FloconTheme.colorPalette.primary), + ) +} + +// --------------------------------------------------------------------------- +// Shared helpers +// --------------------------------------------------------------------------- + @Composable private fun SettingsButton( onClick: () -> Unit, diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsUiState.kt index 393425b0e..456e85e4c 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsUiState.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsUiState.kt @@ -6,7 +6,6 @@ import io.github.openflocon.domain.settings.repository.AdbForwardStatus import io.github.openflocon.flocondesktop.common.log.LogEntryUiModel import io.github.openflocon.flocondesktop.common.log.LogLevel -@Immutable data class SettingsUiState( val fontSizeMultiplier: Float, val logs: List, diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt index 1ecd360d4..c731b2ce0 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt @@ -56,6 +56,11 @@ class SettingsViewModel( logs = logs.map { it.toUiModel() }, adbForwardStatus = forwardStatus, ) + val uiState = combine(fontSizeMultiplierUseCase(), logManager.logs) { multiplier, logs -> + SettingsUiState( + fontSizeMultiplier = multiplier, + logs = logs.map { it.toUiModel() }, + ) } .stateIn( scope = viewModelScope, diff --git a/FloconDesktop/gradle/libs.versions.toml b/FloconDesktop/gradle/libs.versions.toml index 518fcee2d..6320e569f 100644 --- a/FloconDesktop/gradle/libs.versions.toml +++ b/FloconDesktop/gradle/libs.versions.toml @@ -18,7 +18,7 @@ ktor = "3.5.0" logback = "1.5.18" material3-adaptive = "1.3.0-beta02" multiplatform-settings = "1.3.0" -navigation3 = "1.1.1" +navigation3 = "1.2.0-alpha02" opencsv = "5.12.0" other-jsontree = "2.7.0" paging = "3.5.0" From 0509e395d4e8987bbb8766378ecddd1076a6f99e Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Wed, 8 Jul 2026 10:21:15 +0200 Subject: [PATCH 2/6] chore: downgrade navigation3 version to 1.1.1 Reverts the navigation3 library to the stable 1.1.1 release to resolve compatibility or stability issues encountered with the alpha version. --- FloconDesktop/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FloconDesktop/gradle/libs.versions.toml b/FloconDesktop/gradle/libs.versions.toml index 6320e569f..518fcee2d 100644 --- a/FloconDesktop/gradle/libs.versions.toml +++ b/FloconDesktop/gradle/libs.versions.toml @@ -18,7 +18,7 @@ ktor = "3.5.0" logback = "1.5.18" material3-adaptive = "1.3.0-beta02" multiplatform-settings = "1.3.0" -navigation3 = "1.2.0-alpha02" +navigation3 = "1.1.1" opencsv = "5.12.0" other-jsontree = "2.7.0" paging = "3.5.0" From 4ccc0e4176d89e4151cf321e9eac07e80bf00179 Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Mon, 20 Jul 2026 14:32:01 +0200 Subject: [PATCH 3/6] feat: add ADB forward status monitoring to Settings screen Introduces an `AdbForwardStatus` to track the health of reverse port forwarding in the `SettingsRepository`. Updates the Settings UI to display the current connection status and refines log entries with timestamps for better debugging. --- .../app/ui/settings/SettingsScreen.kt | 121 +++++++++++++++--- .../app/ui/settings/SettingsUiState.kt | 1 + .../app/ui/settings/SettingsViewModel.kt | 1 + 3 files changed, 103 insertions(+), 20 deletions(-) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt index 53114c62f..0ea9c3683 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt @@ -23,6 +23,8 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Cable import androidx.compose.material.icons.outlined.Check @@ -437,9 +439,14 @@ private fun AdbPane( modifier = Modifier.weight(1f) ) Text( - text = stringResource(Res.string.settings_adb_valid), - color = FloconTheme.colorPalette.onAccent, + text = when (adbForwardStatus) { + AdbForwardStatus.OK -> "Reverse port forwarding is active and healthy." + AdbForwardStatus.NOK -> "Connection failed. Please ensure ADB is configured correctly and your device is connected." + AdbForwardStatus.UNKNOWN -> "Status unknown. Waiting for device or forwarding loop to initialize." + }, + color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.8f), style = FloconTheme.typography.bodySmall, + modifier = Modifier.weight(1f) ) } } @@ -662,6 +669,13 @@ private fun LogsPane( } } +private data class BadgeTheme( + val label: String, + val bgColor: Color, + val textColor: Color, + val icon: ImageVector +) + @Composable private fun AboutPane( modifier: Modifier = Modifier, @@ -701,25 +715,60 @@ private fun LogsPane( onClearLogs: () -> Unit, modifier: Modifier = Modifier, ) { + val clipboardManager = LocalClipboardManager.current + Column( - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), modifier = modifier.fillMaxSize() ) { Row( - horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Text( - text = "Console (${logs.size})", + text = "Flocon System Logs", style = FloconTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, color = FloconTheme.colorPalette.onPrimary, + modifier = Modifier.weight(1f) ) + if (logs.isNotEmpty()) { - SettingsButton( - text = "Clear", + FloconButton( + onClick = { + val text = logs.joinToString("\n") { "[${it.timestamp} ${it.level.name}] ${it.message}" } + clipboardManager.setText(AnnotatedString(text)) + }, + containerColor = FloconTheme.colorPalette.secondary, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + FloconIcon( + imageVector = Icons.Outlined.ContentCopy, + modifier = Modifier.size(14.dp) + ) + Text("Copy All", style = FloconTheme.typography.bodySmall) + } + } + + FloconButton( onClick = onClearLogs, - ) + containerColor = FloconTheme.colorPalette.secondary, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + FloconIcon( + imageVector = Icons.Outlined.Delete, + modifier = Modifier.size(14.dp) + ) + Text("Clear", style = FloconTheme.typography.bodySmall) + } + } } } @@ -729,17 +778,31 @@ private fun LogsPane( modifier = Modifier .fillMaxWidth() .weight(1f) + .clip(FloconTheme.shapes.medium) + .background(FloconTheme.colorPalette.primary) ) { - Text( - text = "No logs yet", - style = FloconTheme.typography.bodySmall, - color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.4f), - ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + FloconIcon( + imageVector = Icons.Outlined.Info, + tint = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.3f), + modifier = Modifier.size(36.dp) + ) + Text( + text = "No system logs generated yet", + style = FloconTheme.typography.bodyMedium, + color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.4f), + ) + } } } else { ConsoleLogPanel( logs = logs, - modifier = Modifier.fillMaxWidth().weight(1f), + modifier = Modifier + .fillMaxWidth() + .weight(1f), ) } } @@ -749,11 +812,29 @@ private fun LogsPane( private fun AboutPane( modifier: Modifier = Modifier, ) { - AboutScreen( - modifier = modifier - .fillMaxSize() - .background(FloconTheme.colorPalette.primary), - ) + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = modifier.fillMaxSize() + ) { + Text( + text = "Open Source Licenses", + style = FloconTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = FloconTheme.colorPalette.onPrimary, + ) + Text( + text = "Flocon is built using open source software. The licenses of libraries used in this project are listed below.", + style = FloconTheme.typography.bodySmall, + color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.7f), + ) + AboutScreen( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .clip(FloconTheme.shapes.medium) + .background(FloconTheme.colorPalette.primary), + ) + } } // --------------------------------------------------------------------------- diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsUiState.kt index 456e85e4c..393425b0e 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsUiState.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsUiState.kt @@ -6,6 +6,7 @@ import io.github.openflocon.domain.settings.repository.AdbForwardStatus import io.github.openflocon.flocondesktop.common.log.LogEntryUiModel import io.github.openflocon.flocondesktop.common.log.LogLevel +@Immutable data class SettingsUiState( val fontSizeMultiplier: Float, val logs: List, diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt index c731b2ce0..8c70a00a3 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt @@ -60,6 +60,7 @@ class SettingsViewModel( SettingsUiState( fontSizeMultiplier = multiplier, logs = logs.map { it.toUiModel() }, + adbForwardStatus = forwardStatus, ) } .stateIn( From 985983c0ddebb0b4fa179a1dea553eb9fa1aec2d Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Mon, 20 Jul 2026 14:42:45 +0200 Subject: [PATCH 4/6] feat: redesign settings appearance pane and theme selection Refactor the appearance settings to use a card-based layout and update the theme selector with improved visual feedback, including hover states and descriptive icons for each theme option. --- .../openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt index 0ea9c3683..d682194a2 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt @@ -23,8 +23,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Cable import androidx.compose.material.icons.outlined.Check From ccba23730d4d3ff60ae226ebfc786bdc99b9cdf5 Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Mon, 20 Jul 2026 15:34:02 +0200 Subject: [PATCH 5/6] feat: implement onboarding flow and improve ADB error handling Introduce an initial onboarding wizard to guide users through ADB and SDK configuration. Add system-wide ADB health monitoring, including a new error state in the top bar to alert users of connection issues and provide direct access to the configuration settings. --- .../composeResources/values/strings.xml | 2 +- .../openflocon/flocondesktop/AppWindow.kt | 28 +- .../openflocon/flocondesktop/app/AppScreen.kt | 14 +- .../flocondesktop/app/AppViewModel.kt | 25 +- .../app/ui/settings/SettingsScreen.kt | 238 +++++-- .../app/ui/settings/SettingsUiState.kt | 3 +- .../app/ui/settings/SettingsViewModel.kt | 72 ++- .../app/ui/view/topbar/MainScreenTopBar.kt | 67 +- .../flocondesktop/common/utils/FilePicker.kt | 3 + .../data/settings/SettingsRepositoryImpl.kt | 6 + .../datasource/local/SettingsDataSource.kt | 3 + .../local/SettingsDataSourcePrefs.kt | 7 + .../flocondesktop/features/FeaturesModule.kt | 2 + .../flocondesktop/features/network/DI.kt | 2 - .../flocondesktop/features/onboarding/DI.kt | 8 + .../features/onboarding/Navigation.kt | 18 + .../onboarding/OnboardingViewModel.kt | 140 +++++ .../onboarding/view/OnboardingScreen.kt | 591 ++++++++++++++++++ .../messages/ui/MessagesServerDelegate.kt | 37 +- .../messages/ui/di/MessagesUiModule.kt | 4 +- .../common/utils/FilePicker.desktop.kt | 18 + .../domain/adb/ExecuteAdbCommandUseCase.kt | 3 +- .../settings/repository/SettingsRepository.kt | 3 + .../domain/settings/usecase/TestAdbUseCase.kt | 3 +- 24 files changed, 1200 insertions(+), 97 deletions(-) create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/utils/FilePicker.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/DI.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/Navigation.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/OnboardingViewModel.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/view/OnboardingScreen.kt create mode 100644 FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/common/utils/FilePicker.desktop.kt diff --git a/FloconDesktop/composeApp/src/commonMain/composeResources/values/strings.xml b/FloconDesktop/composeApp/src/commonMain/composeResources/values/strings.xml index bcf94b0df..eb554de41 100644 --- a/FloconDesktop/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/FloconDesktop/composeApp/src/commonMain/composeResources/values/strings.xml @@ -87,7 +87,7 @@ Success, file saved at %1$s Search Please setup ADB first, this field is mandatory - ADB configuraton is valid + ADB configuration is valid Font Size Multiplier : %1$sx Theme Dark diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/AppWindow.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/AppWindow.kt index 18821305e..2cfac6fcd 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/AppWindow.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/AppWindow.kt @@ -21,10 +21,15 @@ import io.github.openflocon.flocondesktop.app.AppScreen import io.github.openflocon.flocondesktop.app.di.appModule import io.github.openflocon.flocondesktop.common.di.commonModule import io.github.openflocon.flocondesktop.core.di.coreModule +import io.github.openflocon.domain.settings.repository.SettingsRepository +import io.github.openflocon.domain.device.repository.DevicesRepository +import io.github.openflocon.flocondesktop.features.onboarding.OnboardingRoutes import io.github.openflocon.flocondesktop.features.featuresModule import io.github.openflocon.flocondesktop.features.network.NetworkRoutes import io.github.openflocon.library.designsystem.FloconTheme import io.github.openflocon.navigation.MainFloconNavigationState +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import org.koin.compose.KoinApplication import org.koin.compose.koinInject import org.koin.core.module.dsl.singleOf @@ -51,7 +56,28 @@ fun App() { // scope { // scoped { MainFloconNavigationState(MainRoutes.Main) } // } - single { MainFloconNavigationState(NetworkRoutes.Main) } + single { + val repository = get() + val devicesRepository = get() + val hasDevices = try { + runBlocking { + devicesRepository.devices.first().isNotEmpty() + } + } catch (e: Exception) { + false + } + if (hasDevices && !repository.isOnboardingCompleted()) { + runBlocking { + repository.setOnboardingCompleted(true) + } + } + val startRoute = if (repository.isOnboardingCompleted() || hasDevices) { + NetworkRoutes.Main + } else { + OnboardingRoutes.Main + } + MainFloconNavigationState(startRoute) + } singleOf(::AdbRepositoryImpl) bind AdbRepository::class }, ) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/AppScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/AppScreen.kt index 2e9556ce0..ec5bbe497 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/AppScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/AppScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.scene.SinglePaneSceneStrategy import io.github.openflocon.flocondesktop.app.ui.settings.settingsRoutes +import io.github.openflocon.flocondesktop.app.ui.settings.SettingsRoutes import io.github.openflocon.flocondesktop.app.ui.view.leftpannel.LeftPanelView import io.github.openflocon.flocondesktop.app.ui.view.topbar.MainScreenTopBar import io.github.openflocon.flocondesktop.app.version.VersionCheckerView @@ -23,6 +24,7 @@ import io.github.openflocon.flocondesktop.features.deeplinks.deeplinkRoutes import io.github.openflocon.flocondesktop.features.files.filesRoutes import io.github.openflocon.flocondesktop.features.images.imageRoutes import io.github.openflocon.flocondesktop.features.network.networkRoutes +import io.github.openflocon.flocondesktop.features.onboarding.onboardingRoutes import io.github.openflocon.flocondesktop.features.sharedpreferences.sharedPreferencesRoutes import io.github.openflocon.flocondesktop.features.table.tableRoutes import io.github.openflocon.library.designsystem.FloconTheme @@ -38,10 +40,14 @@ import org.koin.compose.viewmodel.koinViewModel fun AppScreen() { val viewModel = koinViewModel() val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val adbError by viewModel.adbErrorState.collectAsStateWithLifecycle() + val serverError by viewModel.serverError.collectAsStateWithLifecycle() Box(modifier = Modifier.fillMaxSize()) { Content( uiState = uiState, + adbError = adbError, + serverError = serverError, navigationState = viewModel.navigationState, onAction = viewModel::onAction ) @@ -53,6 +59,8 @@ fun AppScreen() { @Composable private fun Content( uiState: AppUiState, + adbError: AdbErrorType, + serverError: String?, navigationState: MainFloconNavigationState, onAction: (AppAction) -> Unit ) { @@ -90,7 +98,10 @@ private fun Content( onAppSelected = { onAction(AppAction.SelectApp(it)) }, onRecordClicked = { onAction(AppAction.Record) }, onRestartClicked = { onAction(AppAction.Restart) }, - onTakeScreenshotClicked = { onAction(AppAction.Screenshoot) } + onTakeScreenshotClicked = { onAction(AppAction.Screenshoot) }, + adbError = adbError, + serverError = serverError, + onFixAdbClicked = { navigationState.navigate(SettingsRoutes.Main) } ) } ) @@ -111,5 +122,6 @@ private fun Content( tableRoutes() settingsRoutes() crashReporterRoutes() + onboardingRoutes() } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/AppViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/AppViewModel.kt index ec086a369..7356d5afe 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/AppViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/AppViewModel.kt @@ -36,12 +36,16 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString +enum class AdbErrorType { NONE, SETUP_REQUIRED, PORT_FORWARD_ERROR, SERVER_ERROR } + internal class AppViewModel( - messagesServerDelegate: MessagesServerDelegate, + private val messagesServerDelegate: MessagesServerDelegate, initAdbPathUseCase: InitAdbPathUseCase, startAdbForwardUseCase: StartAdbForwardUseCase, val navigationState: MainFloconNavigationState, @@ -56,6 +60,25 @@ internal class AppViewModel( private val settingsRepository: SettingsRepository, ) : ViewModel(messagesServerDelegate) { + val serverError = messagesServerDelegate.serverError + + val adbErrorState = combine( + initialSetupStateHolder.needsAdbSetup, + settingsRepository.adbForwardStatus, + messagesServerDelegate.serverError + ) { needsSetup, forwardStatus, serverErrorMsg -> + when { + serverErrorMsg != null -> AdbErrorType.SERVER_ERROR + needsSetup -> AdbErrorType.SETUP_REQUIRED + forwardStatus == AdbForwardStatus.NOK -> AdbErrorType.PORT_FORWARD_ERROR + else -> AdbErrorType.NONE + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = AdbErrorType.NONE + ) + private val contentState = MutableStateFlow( ContentUiState( current = SubScreen.Network diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt index d682194a2..58e16517f 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt @@ -73,6 +73,11 @@ import io.github.openflocon.flocondesktop.common.log.LogLevel import io.github.openflocon.library.designsystem.FloconTheme import io.github.openflocon.library.designsystem.components.FloconButton import io.github.openflocon.library.designsystem.components.FloconIcon +import androidx.compose.runtime.rememberCoroutineScope +import kotlinx.coroutines.launch +import io.github.openflocon.flocondesktop.common.utils.pickAdbFile +import io.github.openflocon.library.designsystem.components.FloconIconButton +import androidx.compose.material.icons.outlined.FolderOpen import io.github.openflocon.library.designsystem.components.FloconSlider import io.github.openflocon.library.designsystem.components.FloconSurface import io.github.openflocon.library.designsystem.components.FloconTextFieldWithoutM3 @@ -116,6 +121,8 @@ fun SettingsScreen( onAction = viewModel::onAction, onClearLogs = viewModel::clearLogs, needsAdbSetup = needsAdbSetup, + onLaunchOnboarding = viewModel::launchOnboarding, + onRelaunchAdbAndServer = viewModel::relaunchAdbAndServer, ) } @@ -133,6 +140,8 @@ private fun SettingsScreen( needsAdbSetup: Boolean, onAction: (SettingsAction) -> Unit, onClearLogs: () -> Unit, + onLaunchOnboarding: () -> Unit, + onRelaunchAdbAndServer: () -> Unit, modifier: Modifier = Modifier, ) { var selectedTab by remember { mutableStateOf(SettingsTab.Adb) } @@ -174,6 +183,8 @@ private fun SettingsScreen( testAdbPath = testAdbPath, needsAdbSetup = needsAdbSetup, adbForwardStatus = uiState.adbForwardStatus, + serverError = uiState.serverError, + onRelaunchAdbAndServer = onRelaunchAdbAndServer, ) SettingsTab.Appearance -> AppearancePane( @@ -187,7 +198,9 @@ private fun SettingsScreen( onClearLogs = onClearLogs, ) - SettingsTab.About -> AboutPane() + SettingsTab.About -> AboutPane( + onLaunchOnboarding = onLaunchOnboarding + ) } } } @@ -323,8 +336,11 @@ private fun AdbPane( testAdbPath: () -> Unit, needsAdbSetup: Boolean, adbForwardStatus: AdbForwardStatus, + serverError: String?, + onRelaunchAdbAndServer: () -> Unit, modifier: Modifier = Modifier, ) { + val scope = rememberCoroutineScope() Column( verticalArrangement = Arrangement.spacedBy(16.dp), modifier = modifier @@ -334,7 +350,49 @@ private fun AdbPane( icon = Icons.Outlined.Settings, description = "Flocon communicates with Android devices using the Android Debug Bridge (ADB). Set the path to your adb binary below." ) { - // Setup alert or status + Text( + text = "ADB Executable Path", + style = FloconTheme.typography.labelSmall, + color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.6f) + ) + + FloconTextFieldWithoutM3( + value = adbPathText, + onValueChange = onAdbPathChanged, + placeholder = defaultPlaceHolder("Eg: /Users/youruser/Library/Android/sdk/platform-tools/adb"), + containerColor = FloconTheme.colorPalette.secondary, + contentPadding = PaddingValues(12.dp), + trailingComponent = { + FloconIconButton( + onClick = { + scope.launch { + pickAdbFile()?.let { onAdbPathChanged(it) } + } + } + ) { + FloconIcon( + imageVector = Icons.Outlined.FolderOpen, + tint = FloconTheme.colorPalette.onSecondary + ) + } + }, + modifier = Modifier.fillMaxWidth() + ) + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + SettingsButton( + text = stringResource(Res.string.general_save), + onClick = saveAdbPath, + ) + SettingsButton( + text = stringResource(Res.string.settings_test), + onClick = testAdbPath, + ) + } + + Spacer(Modifier.height(4.dp)) + + // Setup alert or status at the bottom if (needsAdbSetup) { Row( modifier = Modifier @@ -380,72 +438,105 @@ private fun AdbPane( ) } } - - Spacer(Modifier.height(4.dp)) - - Text( - text = "ADB Executable Path", - style = FloconTheme.typography.labelSmall, - color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.6f) - ) - - FloconTextFieldWithoutM3( - value = adbPathText, - onValueChange = onAdbPathChanged, - placeholder = defaultPlaceHolder("Eg: /Users/youruser/Library/Android/sdk/platform-tools/adb"), - containerColor = FloconTheme.colorPalette.secondary, - contentPadding = PaddingValues(12.dp), - modifier = Modifier.fillMaxWidth() - ) - - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - SettingsButton( - text = stringResource(Res.string.general_save), - onClick = saveAdbPath, - ) - SettingsButton( - text = stringResource(Res.string.settings_test), - onClick = testAdbPath, - ) - } } SettingsCard( - title = "ADB Reverse Port Forwarding", + title = "ADB Health Status", icon = Icons.Outlined.Cable, - description = "Flocon runs a local server that communicates with the daemon on the device. Reverse port forwarding enables high-throughput data transfer (logs, preferences, screenshots)." + description = "Flocon runs a local server and uses ADB reverse port forwarding to transfer data (logs, preferences, screenshots) from your Android device." ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier - .fillMaxWidth() - .clip(FloconTheme.shapes.small) - .background(FloconTheme.colorPalette.secondary) - .padding(12.dp) + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() ) { - AdbForwardStatusBadge(status = adbForwardStatus) + // Status 1: Local Server + val serverStarted = serverError == null + val serverBgColor = if (serverStarted) FloconTheme.colorPalette.secondary else FloconTheme.colorPalette.error.copy(alpha = 0.12f) + val serverTextColor = if (serverStarted) FloconTheme.colorPalette.onPrimary else FloconTheme.colorPalette.error + val serverBadgeBgColor = if (serverStarted) FloconTheme.colorPalette.accent.copy(alpha = 0.2f) else FloconTheme.colorPalette.error.copy(alpha = 0.2f) + val serverBadgeTextColor = if (serverStarted) FloconTheme.colorPalette.onAccent else FloconTheme.colorPalette.error - Text( - text = when (adbForwardStatus) { - AdbForwardStatus.OK -> "Reverse port forwarding is active and healthy." - AdbForwardStatus.NOK -> "Connection failed. Please ensure ADB is configured correctly and your device is connected." - AdbForwardStatus.UNKNOWN -> "Status unknown. Waiting for device or forwarding loop to initialize." - }, - color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.8f), - style = FloconTheme.typography.bodySmall, - modifier = Modifier.weight(1f) - ) - Text( - text = when (adbForwardStatus) { - AdbForwardStatus.OK -> "Reverse port forwarding is active and healthy." - AdbForwardStatus.NOK -> "Connection failed. Please ensure ADB is configured correctly and your device is connected." - AdbForwardStatus.UNKNOWN -> "Status unknown. Waiting for device or forwarding loop to initialize." - }, - color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.8f), - style = FloconTheme.typography.bodySmall, - modifier = Modifier.weight(1f) - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .clip(FloconTheme.shapes.small) + .background(serverBgColor) + .padding(12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier + .clip(FloconTheme.shapes.small) + .background(serverBadgeBgColor) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + FloconIcon( + imageVector = if (serverStarted) Icons.Outlined.Check else Icons.Outlined.ErrorOutline, + tint = serverBadgeTextColor, + modifier = Modifier.size(14.dp) + ) + Text( + text = if (serverStarted) "STARTED" else "FAILED", + color = serverBadgeTextColor, + style = FloconTheme.typography.labelSmall, + fontWeight = FontWeight.Bold + ) + } + + Text( + text = if (serverStarted) "Local WebSocket server is running successfully on port 9023." else serverError ?: "Local server failed to start.", + color = serverTextColor, + style = FloconTheme.typography.bodySmall, + modifier = Modifier.weight(1f) + ) + } + + // Status 2: ADB Reverse Port Forwarding + val forwardBgColor = when (adbForwardStatus) { + AdbForwardStatus.NOK -> FloconTheme.colorPalette.error.copy(alpha = 0.12f) + else -> FloconTheme.colorPalette.secondary + } + val forwardTextColor = when (adbForwardStatus) { + AdbForwardStatus.NOK -> FloconTheme.colorPalette.error + AdbForwardStatus.UNKNOWN -> FloconTheme.colorPalette.onSecondary.copy(alpha = 0.8f) + else -> FloconTheme.colorPalette.onPrimary + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .clip(FloconTheme.shapes.small) + .background(forwardBgColor) + .padding(12.dp) + ) { + AdbForwardStatusBadge(status = adbForwardStatus) + + Text( + text = when (adbForwardStatus) { + AdbForwardStatus.OK -> "Reverse port forwarding is active and healthy." + AdbForwardStatus.NOK -> "Connection failed. Please ensure ADB is configured correctly and your device is connected." + AdbForwardStatus.UNKNOWN -> "Status unknown. Waiting for device or forwarding loop to initialize." + }, + color = forwardTextColor, + style = FloconTheme.typography.bodySmall, + modifier = Modifier.weight(1f) + ) + } + + // Relaunch / Retry Button + FloconButton( + onClick = onRelaunchAdbAndServer, + modifier = Modifier.align(Alignment.End) + ) { + Text( + text = "Relaunch Services", + style = FloconTheme.typography.labelMedium + ) + } } } } @@ -808,12 +899,31 @@ private fun LogsPane( @Composable private fun AboutPane( + onLaunchOnboarding: () -> Unit, modifier: Modifier = Modifier, ) { Column( verticalArrangement = Arrangement.spacedBy(12.dp), modifier = modifier.fillMaxSize() ) { + SettingsCard( + title = "Setup Guide", + icon = Icons.Outlined.Info, + description = "Click below to relaunch the initial onboarding and step-by-step setup guide." + ) { + FloconButton( + onClick = onLaunchOnboarding, + containerColor = FloconTheme.colorPalette.secondary + ) { + Text( + text = "Launch Onboarding", + color = FloconTheme.colorPalette.onSecondary + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + Text( text = "Open Source Licenses", style = FloconTheme.typography.titleMedium, @@ -994,7 +1104,9 @@ private fun SettingsScreenPreview() { modifier = Modifier.fillMaxSize(), onAction = {}, onClearLogs = {}, + onLaunchOnboarding = {}, needsAdbSetup = false, + onRelaunchAdbAndServer = {}, ) } } @@ -1013,7 +1125,9 @@ private fun SettingsScreenPreview_needsAdbSetup() { modifier = Modifier.fillMaxSize(), onAction = {}, onClearLogs = {}, + onLaunchOnboarding = {}, needsAdbSetup = true, + onRelaunchAdbAndServer = {}, ) } } @@ -1031,7 +1145,9 @@ private fun SettingsScreen_LogsPreview() { modifier = Modifier.fillMaxSize(), onAction = {}, onClearLogs = {}, + onLaunchOnboarding = {}, needsAdbSetup = false, + onRelaunchAdbAndServer = {}, ) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsUiState.kt index 393425b0e..e0b75fbda 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsUiState.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsUiState.kt @@ -11,7 +11,8 @@ data class SettingsUiState( val fontSizeMultiplier: Float, val logs: List, val adbForwardStatus: AdbForwardStatus, - val theme: ThemeSetting + val theme: ThemeSetting, + val serverError: String? = null ) fun previewSettingsUiState() = SettingsUiState( diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt index 8c70a00a3..f5bbf7fe9 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt @@ -25,6 +25,10 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import io.github.openflocon.navigation.MainFloconNavigationState +import io.github.openflocon.flocondesktop.features.onboarding.OnboardingRoutes +import io.github.openflocon.domain.settings.usecase.StartAdbForwardUseCase +import io.github.openflocon.flocondesktop.messages.ui.MessagesServerDelegate import org.jetbrains.compose.resources.getString class SettingsViewModel( @@ -38,6 +42,9 @@ class SettingsViewModel( private val initialSetupStateHolder: InitialSetupStateHolder, private val dispatcherProvider: DispatcherProvider, private val logManager: LogManager, + private val navigationState: MainFloconNavigationState, + private val startAdbForwardUseCase: StartAdbForwardUseCase, + private val messagesServerDelegate: MessagesServerDelegate, ) : ViewModel() { private val _adbPathInput = MutableStateFlow("") @@ -49,12 +56,14 @@ class SettingsViewModel( observeThemeUseCase(), logManager.logs, settingsRepository.adbForwardStatus, - ) { multiplier, theme, logs, forwardStatus -> + messagesServerDelegate.serverError, + ) { multiplier, theme, logs, forwardStatus, serverErrorMsg -> SettingsUiState( fontSizeMultiplier = multiplier, theme = theme, logs = logs.map { it.toUiModel() }, adbForwardStatus = forwardStatus, + serverError = serverErrorMsg, ) val uiState = combine(fontSizeMultiplierUseCase(), logManager.logs) { multiplier, logs -> SettingsUiState( @@ -70,15 +79,25 @@ class SettingsViewModel( fontSizeMultiplier = 1f, theme = ThemeSetting.DEFAULT, logs = emptyList(), - adbForwardStatus = AdbForwardStatus.UNKNOWN + adbForwardStatus = AdbForwardStatus.UNKNOWN, + serverError = null ) ) init { viewModelScope.launch { - // Utiliser GlobalScope ici pour la simplicité de l'exemple, mais préférez un scope dédié settingsRepository.adbPath.collect { path -> - path?.let { _adbPathInput.value = it } + path?.let { + _adbPathInput.value = it + testAdbUseCase(it).fold( + doOnFailure = { + initialSetupStateHolder.setRequiresInitialSetup() + }, + doOnSuccess = { + initialSetupStateHolder.setAdbIsWorking() + } + ) + } } } } @@ -108,7 +127,19 @@ class SettingsViewModel( fun saveAdbPath() { viewModelScope.launch(dispatcherProvider.viewModel) { - saveAdb() + val path = adbPathInput.value + testAdbUseCase(path).fold( + doOnFailure = { + feedbackDisplayer.displayMessage( + message = "Cannot save: ADB path is invalid.", + type = FeedbackDisplayer.MessageType.Error + ) + }, + doOnSuccess = { + saveAdb() + feedbackDisplayer.displayMessage("ADB path saved successfully!") + } + ) } } @@ -121,11 +152,10 @@ class SettingsViewModel( fun testAdbPath() { viewModelScope.launch(dispatcherProvider.viewModel) { - saveAdb() val path = adbPathInput.value Logger.d(TAG) { "Testing ADB path: $path" } logManager.d(TAG, "Testing ADB path: $path") - testAdbUseCase().fold( + testAdbUseCase(path).fold( doOnFailure = { val msg = "ADB test failed: ${it.message}" Logger.e(TAG, it) { msg } @@ -141,15 +171,43 @@ class SettingsViewModel( logManager.d(TAG, "ADB test succeeded") feedbackDisplayer.displayMessage(getString(Res.string.general_success)) initialSetupStateHolder.setAdbIsWorking() + saveAdb() }, ) } } + fun launchOnboarding() { + viewModelScope.launch { + settingsRepository.setOnboardingCompleted(false) + navigationState.navigate(OnboardingRoutes.Main) + } + } + fun clearLogs() { logManager.clear() } + fun relaunchAdbAndServer() { + viewModelScope.launch(dispatcherProvider.viewModel) { + logManager.d(TAG, "User requested relaunch of ADB Server connection & websocket server") + messagesServerDelegate.relaunchServer() + startAdbForwardUseCase().fold( + doOnSuccess = { + settingsRepository.setAdbForwardStatus(AdbForwardStatus.OK) + feedbackDisplayer.displayMessage("Services relaunched successfully") + }, + doOnFailure = { + settingsRepository.setAdbForwardStatus(AdbForwardStatus.NOK) + feedbackDisplayer.displayMessage( + message = "ADB Port Forward failed: ${it.message}", + type = FeedbackDisplayer.MessageType.Error + ) + } + ) + } + } + companion object { private const val TAG = "SettingsViewModel" } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/MainScreenTopBar.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/MainScreenTopBar.kt index 4a5d43c46..99db97821 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/MainScreenTopBar.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/view/topbar/MainScreenTopBar.kt @@ -28,6 +28,14 @@ import io.github.openflocon.flocondesktop.app.ui.view.topbar.actions.TopBarActio import io.github.openflocon.library.designsystem.FloconTheme import org.jetbrains.compose.resources.painterResource +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Warning +import io.github.openflocon.library.designsystem.components.FloconIcon +import io.github.openflocon.library.designsystem.components.FloconTextButton +import androidx.compose.ui.graphics.Color + +import io.github.openflocon.flocondesktop.app.AdbErrorType + @Composable fun MainScreenTopBar( modifier: Modifier = Modifier, @@ -41,6 +49,9 @@ fun MainScreenTopBar( recordState: RecordVideoStateUiModel, onRecordClicked: () -> Unit, onRestartClicked: () -> Unit, + adbError: AdbErrorType, + serverError: String?, + onFixAdbClicked: () -> Unit, ) { Row( modifier = modifier @@ -50,14 +61,54 @@ fun MainScreenTopBar( ) { Title() Spacer(modifier = Modifier.width(18.dp)) - TopBarDeviceAndAppView( - devicesState = devicesState, - appsState = appsState, - onDeviceSelected = onDeviceSelected, - onAppSelected = onAppSelected, - deleteDevice = deleteDevice, - deleteApp = deleteApp, - ) + if (adbError != AdbErrorType.NONE) { + val errorMessage = when (adbError) { + AdbErrorType.SERVER_ERROR -> serverError ?: "Server Error" + AdbErrorType.SETUP_REQUIRED -> "ADB Error: Setup is required" + AdbErrorType.PORT_FORWARD_ERROR -> "ADB Error: Port forwarding failed" + else -> "" + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .clip(FloconTheme.shapes.small) + .background(FloconTheme.colorPalette.error.copy(alpha = 0.15f)) + .padding(horizontal = 12.dp, vertical = 6.dp) + ) { + FloconIcon( + imageVector = Icons.Outlined.Warning, + tint = FloconTheme.colorPalette.error, + modifier = Modifier.size(16.dp) + ) + Text( + text = errorMessage, + color = FloconTheme.colorPalette.error, + style = FloconTheme.typography.bodySmall, + fontWeight = FontWeight.Medium + ) + FloconTextButton( + onClick = onFixAdbClicked, + containerColor = FloconTheme.colorPalette.error + ) { + Text( + text = "Configure", + color = FloconTheme.colorPalette.onError, + style = FloconTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + } + } + } else { + TopBarDeviceAndAppView( + devicesState = devicesState, + appsState = appsState, + onDeviceSelected = onDeviceSelected, + onAppSelected = onAppSelected, + deleteDevice = deleteDevice, + deleteApp = deleteApp, + ) + } Spacer(modifier = Modifier.weight(1f)) TopBarActions( onTakeScreenshotClicked = onTakeScreenshotClicked, diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/utils/FilePicker.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/utils/FilePicker.kt new file mode 100644 index 000000000..e90e0c5b8 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/utils/FilePicker.kt @@ -0,0 +1,3 @@ +package io.github.openflocon.flocondesktop.common.utils + +expect suspend fun pickAdbFile(): String? diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/core/data/settings/SettingsRepositoryImpl.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/core/data/settings/SettingsRepositoryImpl.kt index 143382897..116fd8380 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/core/data/settings/SettingsRepositoryImpl.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/core/data/settings/SettingsRepositoryImpl.kt @@ -49,4 +49,10 @@ internal class SettingsRepositoryImpl( override suspend fun setTheme(value: ThemeSetting) { localSettingsDataSource.setTheme(value) } + + override fun isOnboardingCompleted(): Boolean = localSettingsDataSource.isOnboardingCompleted() + + override suspend fun setOnboardingCompleted(completed: Boolean) { + localSettingsDataSource.setOnboardingCompleted(completed) + } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/core/data/settings/datasource/local/SettingsDataSource.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/core/data/settings/datasource/local/SettingsDataSource.kt index b3ed91d61..237486948 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/core/data/settings/datasource/local/SettingsDataSource.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/core/data/settings/datasource/local/SettingsDataSource.kt @@ -15,6 +15,9 @@ internal interface SettingsDataSource { suspend fun setFontSizeMultiplier(value: Float) suspend fun setTheme(value: ThemeSetting) + fun isOnboardingCompleted(): Boolean + suspend fun setOnboardingCompleted(completed: Boolean) + val adbPath: Flow val fontSizeMultiplier: StateFlow val theme: StateFlow diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/core/data/settings/datasource/local/SettingsDataSourcePrefs.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/core/data/settings/datasource/local/SettingsDataSourcePrefs.kt index 6b7fd83bf..2f8bd2f01 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/core/data/settings/datasource/local/SettingsDataSourcePrefs.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/core/data/settings/datasource/local/SettingsDataSourcePrefs.kt @@ -66,6 +66,12 @@ internal class SettingsDataSourcePrefs( settings.putString(THEME, value.name) } + override fun isOnboardingCompleted(): Boolean = settings.getBoolean(ONBOARDING_COMPLETED, false) + + override suspend fun setOnboardingCompleted(completed: Boolean) { + settings.putBoolean(ONBOARDING_COMPLETED, completed) + } + private fun String?.toThemeSetting(): ThemeSetting = this ?.let { name -> ThemeSetting.entries.firstOrNull { it.name == name } } ?: ThemeSetting.DEFAULT @@ -102,6 +108,7 @@ internal class SettingsDataSourcePrefs( private const val ADB_PATH = "adb_path" private const val FONT_SIZE_MULTIPLIER = "font_size_multiplier" private const val THEME = "theme" + private const val ONBOARDING_COMPLETED = "onboarding_completed" private const val NETWORK_SETTINGS = "network_settings" } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/FeaturesModule.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/FeaturesModule.kt index 372722c51..71e2dda4b 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/FeaturesModule.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/FeaturesModule.kt @@ -10,6 +10,7 @@ import io.github.openflocon.flocondesktop.features.deeplinks.deeplinkModule import io.github.openflocon.flocondesktop.features.files.filesModule import io.github.openflocon.flocondesktop.features.images.imagesModule import io.github.openflocon.flocondesktop.features.network.networkModule +import io.github.openflocon.flocondesktop.features.onboarding.onboardingModule import io.github.openflocon.flocondesktop.features.sharedpreferences.sharedPreferencesModule import io.github.openflocon.flocondesktop.features.table.tableModule import io.github.openflocon.flocondesktop.messages.di.messagesModule @@ -30,5 +31,6 @@ val featuresModule = module { adbCommanderModule, settingsModule, crashReporterModule, + onboardingModule, ) } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/DI.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/DI.kt index 311f509f6..bd00fe0df 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/DI.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/DI.kt @@ -12,7 +12,6 @@ import io.github.openflocon.flocondesktop.features.network.mock.processor.Export import io.github.openflocon.flocondesktop.features.network.mock.processor.ImportMocksProcessor import io.github.openflocon.flocondesktop.features.network.search.NetworkSearchViewModel import io.github.openflocon.flocondesktop.features.network.websocket.NetworkWebsocketMockViewModel -import io.github.openflocon.flocondesktop.messages.ui.MessagesServerDelegate import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module @@ -21,7 +20,6 @@ internal val networkModule = module { viewModelOf(::NetworkViewModel) viewModelOf(::NetworkDetailViewModel) - factoryOf(::MessagesServerDelegate) factoryOf(::HeaderDelegate) factoryOf(::OpenBodyDelegate) factoryOf(::NetworkDetailDelegate) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/DI.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/DI.kt new file mode 100644 index 000000000..ba6d4c83b --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/DI.kt @@ -0,0 +1,8 @@ +package io.github.openflocon.flocondesktop.features.onboarding + +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +internal val onboardingModule = module { + viewModelOf(::OnboardingViewModel) +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/Navigation.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/Navigation.kt new file mode 100644 index 000000000..2fc3c167e --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/Navigation.kt @@ -0,0 +1,18 @@ +package io.github.openflocon.flocondesktop.features.onboarding + +import androidx.navigation3.runtime.EntryProviderScope +import io.github.openflocon.flocondesktop.features.onboarding.view.OnboardingScreen +import io.github.openflocon.navigation.FloconRoute +import kotlinx.serialization.Serializable + +sealed interface OnboardingRoutes : FloconRoute { + + @Serializable + data object Main : OnboardingRoutes +} + +fun EntryProviderScope.onboardingRoutes() { + entry { + OnboardingScreen() + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/OnboardingViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/OnboardingViewModel.kt new file mode 100644 index 000000000..e3249a1eb --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/OnboardingViewModel.kt @@ -0,0 +1,140 @@ +package io.github.openflocon.flocondesktop.features.onboarding + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.openflocon.domain.settings.repository.SettingsRepository +import io.github.openflocon.domain.settings.usecase.TestAdbUseCase +import io.github.openflocon.flocondesktop.app.ui.delegates.DevicesDelegate +import io.github.openflocon.flocondesktop.app.ui.model.AppsStateUiModel +import io.github.openflocon.flocondesktop.features.network.NetworkRoutes +import io.github.openflocon.navigation.MainFloconNavigationState +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +enum class OnboardingStep { + AdbConfig, + FloconSetup, + LaunchApp, + Success +} + +class OnboardingViewModel( + private val settingsRepository: SettingsRepository, + private val testAdbUseCase: TestAdbUseCase, + private val devicesDelegate: DevicesDelegate, + private val navigationState: MainFloconNavigationState, +) : ViewModel() { + + private val _currentStep = MutableStateFlow(OnboardingStep.AdbConfig) + val currentStep: StateFlow = _currentStep.asStateFlow() + + private val _adbPathInput = MutableStateFlow("") + val adbPathInput: StateFlow = _adbPathInput.asStateFlow() + + private val _adbValid = MutableStateFlow(null) + val adbValid: StateFlow = _adbValid.asStateFlow() + + private val _isAdbTesting = MutableStateFlow(false) + val isAdbTesting: StateFlow = _isAdbTesting.asStateFlow() + + private val _appDetected = MutableStateFlow(false) + val appDetected: StateFlow = _appDetected.asStateFlow() + + private val _successCountdown = MutableStateFlow(5) + val successCountdown: StateFlow = _successCountdown.asStateFlow() + + init { + viewModelScope.launch { + val path = settingsRepository.getAdbPath() ?: "" + _adbPathInput.value = path + if (path.isNotEmpty()) { + _isAdbTesting.value = true + testAdbUseCase(path).fold( + doOnFailure = { _adbValid.value = false }, + doOnSuccess = { _adbValid.value = true } + ) + _isAdbTesting.value = false + } + } + + viewModelScope.launch { + devicesDelegate.appsState.collect { state -> + val detected = state is AppsStateUiModel.WithApps && state.apps.isNotEmpty() + _appDetected.value = detected + if (detected && _currentStep.value == OnboardingStep.LaunchApp) { + goToStep(OnboardingStep.Success) + } + } + } + } + + fun onAdbPathChanged(newPath: String) { + _adbPathInput.value = newPath + _adbValid.value = null // reset validation state on edit + } + + fun testAdbPath() { + val path = adbPathInput.value + viewModelScope.launch { + _isAdbTesting.value = true + testAdbUseCase(path).fold( + doOnFailure = { + _adbValid.value = false + }, + doOnSuccess = { + _adbValid.value = true + settingsRepository.setAdbPath(path) + } + ) + _isAdbTesting.value = false + } + } + + fun goToStep(step: OnboardingStep) { + _currentStep.value = step + if (step == OnboardingStep.Success) { + startSuccessCountdown() + } + } + + fun nextStep() { + val next = when (_currentStep.value) { + OnboardingStep.AdbConfig -> OnboardingStep.FloconSetup + OnboardingStep.FloconSetup -> OnboardingStep.LaunchApp + OnboardingStep.LaunchApp -> OnboardingStep.Success + OnboardingStep.Success -> OnboardingStep.Success + } + goToStep(next) + } + + fun previousStep() { + val prev = when (_currentStep.value) { + OnboardingStep.AdbConfig -> OnboardingStep.AdbConfig + OnboardingStep.FloconSetup -> OnboardingStep.AdbConfig + OnboardingStep.LaunchApp -> OnboardingStep.FloconSetup + OnboardingStep.Success -> OnboardingStep.LaunchApp + } + goToStep(prev) + } + + fun skipOnboarding() { + viewModelScope.launch { + settingsRepository.setOnboardingCompleted(true) + navigationState.menu(NetworkRoutes.Main) + } + } + + private fun startSuccessCountdown() { + viewModelScope.launch { + settingsRepository.setOnboardingCompleted(true) + for (i in 5 downTo 1) { + _successCountdown.value = i + delay(1000) + } + navigationState.menu(NetworkRoutes.Main) + } + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/view/OnboardingScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/view/OnboardingScreen.kt new file mode 100644 index 000000000..8ae7ffa47 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/onboarding/view/OnboardingScreen.kt @@ -0,0 +1,591 @@ +package io.github.openflocon.flocondesktop.features.onboarding.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.ChevronLeft +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.Error +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +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.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.github.openflocon.flocondesktop.features.onboarding.OnboardingStep +import io.github.openflocon.flocondesktop.features.onboarding.OnboardingViewModel +import io.github.openflocon.library.designsystem.FloconTheme +import io.github.openflocon.library.designsystem.components.FloconButton +import io.github.openflocon.library.designsystem.components.FloconCircularProgressIndicator +import io.github.openflocon.library.designsystem.components.FloconCodeBlock +import io.github.openflocon.library.designsystem.components.FloconIcon +import io.github.openflocon.library.designsystem.components.FloconOutlinedButton +import io.github.openflocon.library.designsystem.components.FloconSurface +import io.github.openflocon.library.designsystem.components.FloconTextField +import io.github.openflocon.library.designsystem.components.FloconTextButton +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.material.icons.outlined.FolderOpen +import io.github.openflocon.flocondesktop.common.utils.pickAdbFile +import io.github.openflocon.library.designsystem.components.FloconIconButton +import kotlinx.coroutines.launch +import org.koin.compose.viewmodel.koinViewModel +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween + +@Composable +fun OnboardingScreen() { + val viewModel = koinViewModel() + val currentStep by viewModel.currentStep.collectAsState() + val adbPathInput by viewModel.adbPathInput.collectAsState() + val adbValid by viewModel.adbValid.collectAsState() + val isAdbTesting by viewModel.isAdbTesting.collectAsState() + val successCountdown by viewModel.successCountdown.collectAsState() + + Box( + modifier = Modifier + .fillMaxSize() + .background(FloconTheme.colorPalette.surface), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp), + modifier = Modifier + .width(600.dp) + .padding(32.dp) + ) { + // Header Title + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Welcome to Flocon", + style = FloconTheme.typography.headlineMedium.copy( + fontWeight = FontWeight.Bold, + color = FloconTheme.colorPalette.onAccent + ) + ) + + if (currentStep != OnboardingStep.Success) { + FloconTextButton( + onClick = viewModel::skipOnboarding, + containerColor = Color.Transparent + ) { + Text( + text = "Close", + color = FloconTheme.colorPalette.onSurface.copy(alpha = 0.6f) + ) + } + } + } + + // Step indicator + StepIndicator(currentStep = currentStep) + + // Main wizard Card + FloconSurface( + color = FloconTheme.colorPalette.primary, + shape = FloconTheme.shapes.large, + modifier = Modifier + .fillMaxWidth() + .animateContentSize(animationSpec = tween(durationMillis = 300)) + ) { + AnimatedContent( + targetState = currentStep, + transitionSpec = { + if (targetState.ordinal > initialState.ordinal) { + (slideInHorizontally(animationSpec = tween(300)) { width -> width } + fadeIn(animationSpec = tween(300))) togetherWith + (slideOutHorizontally(animationSpec = tween(300)) { width -> -width } + fadeOut(animationSpec = tween(300))) + } else { + (slideInHorizontally(animationSpec = tween(300)) { width -> -width } + fadeIn(animationSpec = tween(300))) togetherWith + (slideOutHorizontally(animationSpec = tween(300)) { width -> width } + fadeOut(animationSpec = tween(300))) + }.using( + SizeTransform(clip = false) + ) + } + ) { step -> + Column( + modifier = Modifier.padding(24.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + when (step) { + OnboardingStep.AdbConfig -> AdbConfigStep( + adbPathInput = adbPathInput, + adbValid = adbValid, + isAdbTesting = isAdbTesting, + onPathChange = viewModel::onAdbPathChanged, + onTest = viewModel::testAdbPath, + onNext = viewModel::nextStep + ) + OnboardingStep.FloconSetup -> FloconSetupStep( + onBack = viewModel::previousStep, + onNext = viewModel::nextStep + ) + OnboardingStep.LaunchApp -> LaunchAppStep( + onBack = viewModel::previousStep + ) + OnboardingStep.Success -> SuccessStep( + countdown = successCountdown + ) + } + } + } + } + } + } +} + +@Composable +private fun StepIndicator(currentStep: OnboardingStep) { + val steps = listOf( + "ADB Setup" to OnboardingStep.AdbConfig, + "SDK Setup" to OnboardingStep.FloconSetup, + "Launch App" to OnboardingStep.LaunchApp, + "Connected" to OnboardingStep.Success + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + steps.forEachIndexed { index, (label, step) -> + val isActive = currentStep == step + val isCompleted = currentStep.ordinal > step.ordinal + + val circleColor by animateColorAsState( + targetValue = when { + isActive -> FloconTheme.colorPalette.accent + isCompleted -> FloconTheme.colorPalette.secondary.copy(alpha = 0.5f) + else -> FloconTheme.colorPalette.secondary.copy(alpha = 0.2f) + }, + animationSpec = tween(durationMillis = 300) + ) + + val textColor by animateColorAsState( + targetValue = if (isActive) FloconTheme.colorPalette.onAccent else FloconTheme.colorPalette.onSecondary, + animationSpec = tween(durationMillis = 300) + ) + + val labelColor by animateColorAsState( + targetValue = if (isActive) FloconTheme.colorPalette.onSurface else FloconTheme.colorPalette.onSurface.copy(alpha = 0.6f), + animationSpec = tween(durationMillis = 300) + ) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.weight(1f) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(28.dp) + .clip(MaterialTheme.shapes.small) + .background(circleColor) + ) { + if (isCompleted) { + FloconIcon( + imageVector = Icons.Default.Check, + tint = FloconTheme.colorPalette.onSecondary, + modifier = Modifier.size(16.dp) + ) + } else { + Text( + text = (index + 1).toString(), + style = FloconTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Bold, + color = textColor + ) + ) + } + } + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = label, + style = FloconTheme.typography.bodySmall.copy( + fontWeight = if (isActive) FontWeight.Bold else FontWeight.Normal, + color = labelColor + ), + textAlign = TextAlign.Center + ) + } + if (index < steps.size - 1) { + val lineColor by animateColorAsState( + targetValue = if (currentStep.ordinal > step.ordinal) FloconTheme.colorPalette.secondary.copy(alpha = 0.5f) + else FloconTheme.colorPalette.secondary.copy(alpha = 0.2f), + animationSpec = tween(durationMillis = 300) + ) + Box( + modifier = Modifier + .height(2.dp) + .weight(0.5f) + .background(lineColor) + ) + } + } + } +} + +@Composable +private fun AdbConfigStep( + adbPathInput: String, + adbValid: Boolean?, + isAdbTesting: Boolean, + onPathChange: (String) -> Unit, + onTest: () -> Unit, + onNext: () -> Unit +) { + val scope = rememberCoroutineScope() + + Text( + text = "1. ADB Configuration", + style = FloconTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + + Text( + text = "Flocon communicates with Android devices using the Android Debug Bridge (ADB). Please configure the path to your adb binary to proceed.", + style = FloconTheme.typography.bodyMedium, + color = FloconTheme.colorPalette.onSurface.copy(alpha = 0.8f) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "ADB Executable Path", + style = FloconTheme.typography.labelMedium, + fontWeight = FontWeight.Bold + ) + + FloconTextField( + value = adbPathInput, + onValueChange = onPathChange, + placeholder = { Text("Eg: /Users/youruser/Library/Android/sdk/platform-tools/adb") }, + trailingComponent = { + FloconIconButton( + onClick = { + scope.launch { + pickAdbFile()?.let { onPathChange(it) } + } + } + ) { + FloconIcon( + imageVector = Icons.Outlined.FolderOpen, + tint = FloconTheme.colorPalette.onSecondary + ) + } + }, + modifier = Modifier.fillMaxWidth() + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + FloconButton( + onClick = onTest, + containerColor = FloconTheme.colorPalette.secondary, + modifier = Modifier.height(36.dp) + ) { + if (isAdbTesting) { + FloconCircularProgressIndicator( + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Testing...") + } else { + Text("Test Connection") + } + } + + when (adbValid) { + true -> { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + FloconIcon( + imageVector = Icons.Default.CheckCircle, + tint = Color(0xFF4CAF50), + modifier = Modifier.size(20.dp) + ) + Text( + text = "Connection Successful", + style = FloconTheme.typography.bodyMedium.copy(color = Color(0xFF4CAF50)) + ) + } + } + false -> { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + FloconIcon( + imageVector = Icons.Default.Error, + tint = FloconTheme.colorPalette.error, + modifier = Modifier.size(20.dp) + ) + Text( + text = "Validation Failed", + style = FloconTheme.typography.bodyMedium.copy(color = FloconTheme.colorPalette.error) + ) + } + } + null -> {} + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + FloconButton( + onClick = { if (adbValid == true) onNext() }, + containerColor = if (adbValid == true) FloconTheme.colorPalette.accent else FloconTheme.colorPalette.secondary.copy(alpha = 0.5f), + modifier = Modifier.width(100.dp) + ) { + Text( + text = "Next", + color = if (adbValid == true) FloconTheme.colorPalette.onAccent else FloconTheme.colorPalette.onSecondary.copy(alpha = 0.5f) + ) + Spacer(modifier = Modifier.width(4.dp)) + FloconIcon( + imageVector = Icons.Default.ChevronRight, + tint = if (adbValid == true) FloconTheme.colorPalette.onAccent else FloconTheme.colorPalette.onSecondary.copy(alpha = 0.5f), + modifier = Modifier.size(16.dp) + ) + } + } +} + +@Composable +private fun FloconSetupStep( + onBack: () -> Unit, + onNext: () -> Unit +) { + Text( + text = "2. Integrate Flocon SDK", + style = FloconTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + + Text( + text = "Add Flocon inside your Android app to start intercepting network requests, databases, and logs.", + style = FloconTheme.typography.bodyMedium, + color = FloconTheme.colorPalette.onSurface.copy(alpha = 0.8f) + ) + + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 240.dp) + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Step A: Add Dependency", + style = FloconTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold + ) + FloconCodeBlock( + code = """ + // In your build.gradle.kts (app module): + dependencies { + debugImplementation("io.github.openflocon:flocon:1.4.0") + releaseImplementation("io.github.openflocon:flocon-no-op:1.4.0") + } + """.trimIndent(), + modifier = Modifier.fillMaxWidth(), + containerColor = FloconTheme.colorPalette.secondary + ) + + Text( + text = "Step B: Initialize in Application", + style = FloconTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold + ) + FloconCodeBlock( + code = """ + // In your Application or MainActivity onCreate(): + import io.github.openflocon.flocon.Flocon + + Flocon.initialize(context) + """.trimIndent(), + modifier = Modifier.fillMaxWidth(), + containerColor = FloconTheme.colorPalette.secondary + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + FloconOutlinedButton( + onClick = onBack, + modifier = Modifier.width(100.dp) + ) { + FloconIcon( + imageVector = Icons.Default.ChevronLeft, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text("Back") + } + + FloconButton( + onClick = onNext, + containerColor = FloconTheme.colorPalette.accent, + modifier = Modifier.width(100.dp) + ) { + Text( + text = "Next", + color = FloconTheme.colorPalette.onAccent + ) + Spacer(modifier = Modifier.width(4.dp)) + FloconIcon( + imageVector = Icons.Default.ChevronRight, + tint = FloconTheme.colorPalette.onAccent, + modifier = Modifier.size(16.dp) + ) + } + } +} + +@Composable +private fun LaunchAppStep( + onBack: () -> Unit +) { + Text( + text = "3. Launch Your App", + style = FloconTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + + Text( + text = "Run your Android application on an emulator or a connected physical device.", + style = FloconTheme.typography.bodyMedium, + color = FloconTheme.colorPalette.onSurface.copy(alpha = 0.8f) + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 32.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + FloconCircularProgressIndicator( + modifier = Modifier.size(48.dp) + ) + Text( + text = "Waiting for Flocon app connection...", + style = FloconTheme.typography.bodyMedium, + color = FloconTheme.colorPalette.onSurface.copy(alpha = 0.7f) + ) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + FloconOutlinedButton( + onClick = onBack, + modifier = Modifier.width(100.dp) + ) { + FloconIcon( + imageVector = Icons.Default.ChevronLeft, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text("Back") + } + } +} + +@Composable +private fun SuccessStep( + countdown: Int +) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + FloconIcon( + imageVector = Icons.Default.CheckCircle, + tint = Color(0xFF4CAF50), + modifier = Modifier.size(64.dp) + ) + Text( + text = "Successfully Connected!", + style = FloconTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = Color(0xFF4CAF50) + ) + Text( + text = "Flocon has successfully detected your application and is ready to capture network requests, databases, and logs.", + style = FloconTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = FloconTheme.colorPalette.onSurface.copy(alpha = 0.8f) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Redirecting to network menu in $countdown seconds...", + style = FloconTheme.typography.bodySmall, + color = FloconTheme.colorPalette.onSurface.copy(alpha = 0.6f) + ) + } + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/messages/ui/MessagesServerDelegate.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/messages/ui/MessagesServerDelegate.kt index 921fcd42a..a67938cc6 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/messages/ui/MessagesServerDelegate.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/messages/ui/MessagesServerDelegate.kt @@ -12,6 +12,8 @@ import io.github.openflocon.domain.messages.usecase.StartServerUseCase import io.github.openflocon.flocondesktop.common.coroutines.closeable.CloseableDelegate import io.github.openflocon.flocondesktop.common.coroutines.closeable.CloseableScoped import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -26,6 +28,9 @@ class MessagesServerDelegate( private val dispatcherProvider: DispatcherProvider, ) : CloseableScoped by closeableDelegate { + private val _serverError = MutableStateFlow(null) + val serverError = _serverError.asStateFlow() + fun initialize() { coroutineScope.launch { handleIncomingMessagesUseCase() @@ -44,6 +49,7 @@ class MessagesServerDelegate( while (isActive) { startServer().fold( doOnSuccess = { + _serverError.value = null delay(20.seconds) }, doOnFailure = { @@ -54,20 +60,31 @@ class MessagesServerDelegate( } } + fun relaunchServer() { + coroutineScope.launch { + startServer().fold( + doOnSuccess = { + _serverError.value = null + }, + doOnFailure = { + // error is set inside startServer + } + ) + } + } + private fun startServer(): Either = try { startServerUseCase() Success(Unit) } catch (t: Throwable) { - feedbackDisplayer.displayMessage( - buildString { - append("Cannot start server on port ${Constant.SERVER_WEBSOCKET_PORT}") - t.message?.let { - append(" : ") - append(it) - } - }, - type = FeedbackDisplayer.MessageType.Error, - ) + val errorMsg = buildString { + append("Cannot start server on port ${Constant.SERVER_WEBSOCKET_PORT}") + t.message?.let { + append(" : ") + append(it) + } + } + _serverError.value = errorMsg Failure(t) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/messages/ui/di/MessagesUiModule.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/messages/ui/di/MessagesUiModule.kt index c2ef0fb79..0f14754b1 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/messages/ui/di/MessagesUiModule.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/messages/ui/di/MessagesUiModule.kt @@ -1,10 +1,10 @@ package io.github.openflocon.flocondesktop.messages.ui.di import io.github.openflocon.flocondesktop.messages.ui.MessagesServerDelegate -import org.koin.core.module.dsl.factoryOf +import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val messagesUiModule = module { - factoryOf(::MessagesServerDelegate) + singleOf(::MessagesServerDelegate) } diff --git a/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/common/utils/FilePicker.desktop.kt b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/common/utils/FilePicker.desktop.kt new file mode 100644 index 000000000..4e0e7ea09 --- /dev/null +++ b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/common/utils/FilePicker.desktop.kt @@ -0,0 +1,18 @@ +package io.github.openflocon.flocondesktop.common.utils + +import java.awt.FileDialog +import java.awt.Frame +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +actual suspend fun pickAdbFile(): String? = withContext(Dispatchers.IO) { + val fileDialog = FileDialog(null as Frame?, "Select ADB Executable", FileDialog.LOAD) + fileDialog.isVisible = true + val directory = fileDialog.directory + val file = fileDialog.file + if (directory != null && file != null) { + "$directory$file" + } else { + null + } +} 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/ExecuteAdbCommandUseCase.kt index 24bd3840b..6a2487b7a 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/ExecuteAdbCommandUseCase.kt @@ -13,8 +13,9 @@ class ExecuteAdbCommandUseCase( suspend operator fun invoke( target: AdbCommandTargetDomainModel, command: String, + adbPathOverride: String? = null, ): Either { - val adbPath = settingsRepository.getAdbPath() ?: return Failure(Throwable("No adb path")) + val adbPath = adbPathOverride ?: settingsRepository.getAdbPath() ?: return Failure(Throwable("No adb path")) val deviceSerial = when (target) { is AdbCommandTargetDomainModel.Device -> adbRepository.getAdbSerial(target.deviceId) diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/settings/repository/SettingsRepository.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/settings/repository/SettingsRepository.kt index d170a0d74..465d22a09 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/settings/repository/SettingsRepository.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/settings/repository/SettingsRepository.kt @@ -19,6 +19,9 @@ interface SettingsRepository { suspend fun setTheme(value: ThemeSetting) + fun isOnboardingCompleted(): Boolean + suspend fun setOnboardingCompleted(completed: Boolean) + val adbPath: Flow val fontSizeMultiplier: StateFlow val theme: StateFlow 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 af2298fdd..1b42cc165 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 @@ -7,8 +7,9 @@ import io.github.openflocon.domain.common.Either class TestAdbUseCase( private val executeAdbCommandUseCase: ExecuteAdbCommandUseCase, ) { - suspend operator fun invoke(): Either = executeAdbCommandUseCase( + suspend operator fun invoke(adbPathOverride: String? = null): Either = executeAdbCommandUseCase( command = "start-server", target = AdbCommandTargetDomainModel.AllDevices, + adbPathOverride = adbPathOverride, ).mapSuccess { Unit } } From 266ccd9f20f03db072f5e6be1d3be49ea668577a Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Wed, 29 Jul 2026 19:13:09 +0200 Subject: [PATCH 6/6] feat: prepare settings UI for onboarding integration The dedicated logs pane is removed to streamline the settings interface. The About section is refactored to allow for direct access to the onboarding flow. --- .../app/ui/settings/SettingsScreen.kt | 135 ------------------ .../app/ui/settings/SettingsViewModel.kt | 6 - 2 files changed, 141 deletions(-) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt index 58e16517f..6320e3b8f 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsScreen.kt @@ -542,13 +542,6 @@ private fun AdbPane( } } -private data class BadgeTheme( - val label: String, - val bgColor: Color, - val textColor: Color, - val icon: ImageVector -) - @Composable private fun AdbForwardStatusBadge( status: AdbForwardStatus, @@ -765,138 +758,10 @@ private data class BadgeTheme( val icon: ImageVector ) -@Composable -private fun AboutPane( - modifier: Modifier = Modifier, -) { - Column( - verticalArrangement = Arrangement.spacedBy(12.dp), - modifier = modifier.fillMaxSize() - ) { - Text( - text = "Open Source Licenses", - style = FloconTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = FloconTheme.colorPalette.onPrimary, - ) - Text( - text = "Flocon is built using open source software. The licenses of libraries used in this project are listed below.", - style = FloconTheme.typography.bodySmall, - color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.7f), - ) - AboutScreen( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .clip(FloconTheme.shapes.medium) - .background(FloconTheme.colorPalette.primary), - ) - } -} - // --------------------------------------------------------------------------- // Shared helpers // --------------------------------------------------------------------------- -@Composable -private fun LogsPane( - logs: List, - onClearLogs: () -> Unit, - modifier: Modifier = Modifier, -) { - val clipboardManager = LocalClipboardManager.current - - Column( - verticalArrangement = Arrangement.spacedBy(12.dp), - modifier = modifier.fillMaxSize() - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = "Flocon System Logs", - style = FloconTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = FloconTheme.colorPalette.onPrimary, - modifier = Modifier.weight(1f) - ) - - if (logs.isNotEmpty()) { - FloconButton( - onClick = { - val text = logs.joinToString("\n") { "[${it.timestamp} ${it.level.name}] ${it.message}" } - clipboardManager.setText(AnnotatedString(text)) - }, - containerColor = FloconTheme.colorPalette.secondary, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { - FloconIcon( - imageVector = Icons.Outlined.ContentCopy, - modifier = Modifier.size(14.dp) - ) - Text("Copy All", style = FloconTheme.typography.bodySmall) - } - } - - FloconButton( - onClick = onClearLogs, - containerColor = FloconTheme.colorPalette.secondary, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { - FloconIcon( - imageVector = Icons.Outlined.Delete, - modifier = Modifier.size(14.dp) - ) - Text("Clear", style = FloconTheme.typography.bodySmall) - } - } - } - } - - if (logs.isEmpty()) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .clip(FloconTheme.shapes.medium) - .background(FloconTheme.colorPalette.primary) - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - FloconIcon( - imageVector = Icons.Outlined.Info, - tint = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.3f), - modifier = Modifier.size(36.dp) - ) - Text( - text = "No system logs generated yet", - style = FloconTheme.typography.bodyMedium, - color = FloconTheme.colorPalette.onPrimary.copy(alpha = 0.4f), - ) - } - } - } else { - ConsoleLogPanel( - logs = logs, - modifier = Modifier - .fillMaxWidth() - .weight(1f), - ) - } - } -} - @Composable private fun AboutPane( onLaunchOnboarding: () -> Unit, diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt index f5bbf7fe9..b4d45d307 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/app/ui/settings/SettingsViewModel.kt @@ -65,12 +65,6 @@ class SettingsViewModel( adbForwardStatus = forwardStatus, serverError = serverErrorMsg, ) - val uiState = combine(fontSizeMultiplierUseCase(), logManager.logs) { multiplier, logs -> - SettingsUiState( - fontSizeMultiplier = multiplier, - logs = logs.map { it.toUiModel() }, - adbForwardStatus = forwardStatus, - ) } .stateIn( scope = viewModelScope,