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 d403fac7d..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
@@ -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,74 +438,110 @@ 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)
- )
+ 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
+ )
+ }
}
}
}
}
-private data class BadgeTheme(
- val label: String,
- val bgColor: Color,
- val textColor: Color,
- val icon: ImageVector
-)
-
@Composable
private fun AdbForwardStatusBadge(
status: AdbForwardStatus,
@@ -657,14 +751,44 @@ private fun LogsPane(
}
}
+private data class BadgeTheme(
+ val label: String,
+ val bgColor: Color,
+ val textColor: Color,
+ val icon: ImageVector
+)
+
+// ---------------------------------------------------------------------------
+// Shared helpers
+// ---------------------------------------------------------------------------
+
@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,
@@ -845,7 +969,9 @@ private fun SettingsScreenPreview() {
modifier = Modifier.fillMaxSize(),
onAction = {},
onClearLogs = {},
+ onLaunchOnboarding = {},
needsAdbSetup = false,
+ onRelaunchAdbAndServer = {},
)
}
}
@@ -864,7 +990,9 @@ private fun SettingsScreenPreview_needsAdbSetup() {
modifier = Modifier.fillMaxSize(),
onAction = {},
onClearLogs = {},
+ onLaunchOnboarding = {},
needsAdbSetup = true,
+ onRelaunchAdbAndServer = {},
)
}
}
@@ -882,7 +1010,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 1ecd360d4..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
@@ -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,
)
}
.stateIn(
@@ -64,15 +73,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()
+ }
+ )
+ }
}
}
}
@@ -102,7 +121,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!")
+ }
+ )
}
}
@@ -115,11 +146,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 }
@@ -135,15 +165,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 }
}