feat: First appearance version (#281)

Co-authored-by: TEYSSANDIER Raphael <rteyssandier@sephora.fr>
This commit is contained in:
Raphael Teyssandier 2025-10-01 10:32:57 +02:00 committed by GitHub
parent c2887551d2
commit 5c8c4e0d95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 272 additions and 59 deletions

View file

@ -8,12 +8,15 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flocon.data.remote.dataRemoteModule
import io.github.openflocon.data.core.dataCoreModule
import io.github.openflocon.data.local.dataLocalModule
import io.github.openflocon.domain.adb.repository.AdbRepository
import io.github.openflocon.domain.domainModule
import io.github.openflocon.domain.settings.usecase.ObserveFontSizeMultiplierUseCase
import io.github.openflocon.flocondesktop.adb.AdbRepositoryImpl
import io.github.openflocon.flocondesktop.app.AppViewModel
import io.github.openflocon.flocondesktop.app.di.appModule
@ -26,6 +29,7 @@ import io.github.openflocon.flocondesktop.main.ui.MainScreen
import io.github.openflocon.library.designsystem.FloconTheme
import io.github.openflocon.library.designsystem.components.FloconSurface
import org.koin.compose.KoinApplication
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
@ -54,7 +58,12 @@ fun App() {
)
},
) {
FloconTheme {
val fontSizeMultiplier by koinInject<ObserveFontSizeMultiplierUseCase>()()
.collectAsStateWithLifecycle()
FloconTheme(
fontSizeMultiplier = fontSizeMultiplier
) {
val appViewModel: AppViewModel = koinViewModel()
FloconSurface(

View file

@ -3,15 +3,23 @@ package io.github.openflocon.flocondesktop.core.data.settings
import io.github.openflocon.domain.settings.repository.SettingsRepository
import io.github.openflocon.flocondesktop.core.data.settings.datasource.local.SettingsDataSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
class SettingsRepositoryImpl(
private val localSettingsDataSource: SettingsDataSource,
) : SettingsRepository {
override val adbPath: Flow<String?> = localSettingsDataSource.adbPath
override val fontSizeMultiplier: StateFlow<Float> = localSettingsDataSource.fontSizeMultiplier
override fun getAdbPath(): String? = localSettingsDataSource.getAdbPath()
override suspend fun setAdbPath(path: String) {
localSettingsDataSource.setAdbPath(path)
}
override val adbPath: Flow<String?> = localSettingsDataSource.adbPath
override suspend fun setFontSizeMultiplier(value: Float) {
localSettingsDataSource.setFontSizeMultiplier(value)
}
}

View file

@ -1,11 +1,14 @@
package io.github.openflocon.flocondesktop.core.data.settings.datasource.local
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface SettingsDataSource {
fun getAdbPath(): String?
suspend fun setAdbPath(path: String)
suspend fun setFontSizeMultiplier(value: Float)
val adbPath: Flow<String?>
val fontSizeMultiplier: StateFlow<Float>
}

View file

@ -5,25 +5,44 @@ package io.github.openflocon.flocondesktop.core.data.settings.datasource.local
import com.russhwolf.settings.ExperimentalSettingsApi
import com.russhwolf.settings.ObservableSettings
import com.russhwolf.settings.coroutines.toFlowSettings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.stateIn
// Expect class pour obtenir les Settings de manière multiplateforme
expect fun createSettings(): ObservableSettings
class SettingsDataSourcePrefs : SettingsDataSource {
class SettingsDataSourcePrefs(
applicationScope: CoroutineScope
) : SettingsDataSource {
private val settings = createSettings()
private val flowSettings = settings.toFlowSettings()
override val adbPath: Flow<String?> = flowSettings.getStringOrNullFlow(ADB_PATH)
override val fontSizeMultiplier: StateFlow<Float> = settings.toFlowSettings()
.getFloatOrNullFlow(FONT_SIZE_MULTIPLIER)
.filterNotNull()
.stateIn(
scope = applicationScope,
started = SharingStarted.Lazily,
initialValue = 1f
)
override fun getAdbPath(): String? = settings.getStringOrNull(ADB_PATH)
override suspend fun setAdbPath(path: String) {
settings.putString(ADB_PATH, path)
}
override val adbPath: Flow<String?> = flowSettings.getStringOrNullFlow(ADB_PATH)
override suspend fun setFontSizeMultiplier(value: Float) {
settings.putFloat(FONT_SIZE_MULTIPLIER, value)
}
companion object {
private const val ADB_PATH = "adb_path"
private const val FONT_SIZE_MULTIPLIER = "font_size_multiplier"
}
}

View file

@ -0,0 +1,7 @@
package io.github.openflocon.flocondesktop.main.ui.settings
sealed interface SettingsAction {
data class FontSizeMultiplierChange(val value: Float) : SettingsAction
}

View file

@ -2,7 +2,6 @@ package io.github.openflocon.flocondesktop.main.ui.settings
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.fillMaxSize
@ -22,11 +21,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import io.github.openflocon.library.designsystem.FloconTheme
import io.github.openflocon.library.designsystem.components.FloconButton
import io.github.openflocon.library.designsystem.components.FloconFeature
import io.github.openflocon.library.designsystem.components.FloconIcon
import io.github.openflocon.library.designsystem.components.FloconSection
import io.github.openflocon.library.designsystem.components.FloconSlider
import io.github.openflocon.library.designsystem.components.FloconTextFieldWithoutM3
import io.github.openflocon.library.designsystem.components.defaultPlaceHolder
import org.jetbrains.compose.ui.tooling.preview.Preview
@ -39,79 +41,106 @@ fun SettingsScreen(
val viewModel: SettingsViewModel = koinViewModel()
val needsAdbSetup by viewModel.needsAdbSetup.collectAsState()
val adbPathText by viewModel.adbPathInput.collectAsState()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Box(modifier = modifier) {
SettingsScreen(
modifier = Modifier.fillMaxSize(),
adbPathText = adbPathText,
onAdbPathChanged = viewModel::onAdbPathChanged,
saveAdbPath = viewModel::saveAdbPath,
testAdbPath = viewModel::testAdbPath,
needsAdbSetup = needsAdbSetup,
)
}
SettingsScreen(
uiState = uiState,
modifier = modifier.fillMaxSize(),
adbPathText = adbPathText,
onAdbPathChanged = viewModel::onAdbPathChanged,
saveAdbPath = viewModel::saveAdbPath,
testAdbPath = viewModel::testAdbPath,
onAction = viewModel::onAction,
needsAdbSetup = needsAdbSetup,
)
}
// Main composable for the screen, incorporating the filter bar
@Composable
private fun SettingsScreen(
uiState: SettingsUiState,
adbPathText: String,
onAdbPathChanged: (String) -> Unit,
saveAdbPath: () -> Unit,
testAdbPath: () -> Unit,
needsAdbSetup: Boolean,
onAction: (SettingsAction) -> Unit,
modifier: Modifier = Modifier,
) {
FloconFeature(
modifier = modifier.fillMaxSize()
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.clip(FloconTheme.shapes.medium)
.background(FloconTheme.colorPalette.primary)
.padding(all = 8.dp)
FloconSection(
title = "Adb Path",
initialValue = true
) {
if (needsAdbSetup) {
Text(
text = "Please setup ADB first, this field is mandatory",
color = FloconTheme.colorPalette.onError,
style = FloconTheme.typography.bodySmall,
)
} else {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
FloconIcon(
imageVector = Icons.Outlined.Check,
tint = FloconTheme.colorPalette.onAccent,
modifier = Modifier.size(16.dp)
)
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.padding(8.dp)
.clip(FloconTheme.shapes.medium)
.background(FloconTheme.colorPalette.primary)
.padding(all = 8.dp)
) {
if (needsAdbSetup) {
Text(
text = "ADB configuraton is valid",
color = FloconTheme.colorPalette.onAccent,
style = FloconTheme.typography.bodySmall
text = "Please setup ADB first, this field is mandatory",
color = FloconTheme.colorPalette.onError,
style = FloconTheme.typography.bodySmall,
)
} else {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
FloconIcon(
imageVector = Icons.Outlined.Check,
tint = FloconTheme.colorPalette.onAccent,
modifier = Modifier.size(16.dp)
)
Text(
text = "ADB configuraton is valid",
color = FloconTheme.colorPalette.onAccent,
style = FloconTheme.typography.bodySmall
)
}
}
FloconTextFieldWithoutM3(
value = adbPathText,
onValueChange = onAdbPathChanged,
placeholder = defaultPlaceHolder("Eg: /Users/youruser/Library/Android/sdk/platform-tools/adb"),
containerColor = FloconTheme.colorPalette.secondary,
modifier = Modifier.fillMaxWidth()
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
SettingsButton(
text = "Save",
onClick = saveAdbPath
)
SettingsButton(
onClick = testAdbPath,
text = "Test",
)
}
}
FloconTextFieldWithoutM3(
value = adbPathText,
onValueChange = onAdbPathChanged,
placeholder = defaultPlaceHolder("Eg: /Users/youruser/Library/Android/sdk/platform-tools/adb"),
containerColor = FloconTheme.colorPalette.secondary,
modifier = Modifier.fillMaxWidth()
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
}
FloconSection(
title = "Appearance",
initialValue = true
) {
Column(
modifier = Modifier
.padding(8.dp)
.clip(FloconTheme.shapes.medium)
.background(FloconTheme.colorPalette.primary)
.padding(all = 8.dp)
) {
SettingsButton(
text = "Save",
onClick = saveAdbPath
)
SettingsButton(
onClick = testAdbPath,
text = "Test",
FloconSlider(
value = uiState.fontSizeMultiplier,
onValueChange = { onAction(SettingsAction.FontSizeMultiplierChange(it)) },
valueRange = 0.2f..5f,
modifier = Modifier.fillMaxWidth()
)
}
}
@ -142,6 +171,7 @@ private fun SettingsScreenPreview() {
FloconTheme {
var adbPath by remember { mutableStateOf("/usr/local/bin/adb") }
SettingsScreen(
uiState = previewSettingsUiState(),
adbPathText = adbPath,
onAdbPathChanged = { adbPath = it },
saveAdbPath = {
@ -151,6 +181,7 @@ private fun SettingsScreenPreview() {
Logger.d { "Test ADB FilePathDomainModel: $adbPath" }
},
modifier = Modifier.fillMaxSize(),
onAction = {},
needsAdbSetup = false,
)
}
@ -162,11 +193,13 @@ private fun SettingsScreenPreview_needsAdbSetup() {
FloconTheme {
var adbPath by remember { mutableStateOf("/usr/local/bin/adb") }
SettingsScreen(
uiState = previewSettingsUiState(),
adbPathText = adbPath,
onAdbPathChanged = { adbPath = it },
saveAdbPath = { Logger.d { "Save ADB FilePathDomainModel: $adbPath" } },
testAdbPath = { Logger.d { "Test ADB FilePathDomainModel: $adbPath" } },
modifier = Modifier.fillMaxSize(),
onAction = {},
needsAdbSetup = true,
)
}

View file

@ -0,0 +1,12 @@
package io.github.openflocon.flocondesktop.main.ui.settings
import androidx.compose.runtime.Immutable
@Immutable
data class SettingsUiState(
val fontSizeMultiplier: Float
)
fun previewSettingsUiState() = SettingsUiState(
fontSizeMultiplier = 1f
)

View file

@ -5,15 +5,22 @@ import androidx.lifecycle.viewModelScope
import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.domain.settings.repository.SettingsRepository
import io.github.openflocon.domain.settings.usecase.ObserveFontSizeMultiplierUseCase
import io.github.openflocon.domain.settings.usecase.SetFontSizeMultiplierUseCase
import io.github.openflocon.domain.settings.usecase.TestAdbUseCase
import io.github.openflocon.flocondesktop.app.InitialSetupStateHolder
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class SettingsViewModel(
private val settingsRepository: SettingsRepository,
private val testAdbUseCase: TestAdbUseCase,
fontSizeMultiplierUseCase: ObserveFontSizeMultiplierUseCase,
private val setFontSizeMultiplierUseCase: SetFontSizeMultiplierUseCase,
private val feedbackDisplayer: FeedbackDisplayer,
private val initialSetupStateHolder: InitialSetupStateHolder,
private val dispatcherProvider: DispatcherProvider,
@ -23,6 +30,15 @@ class SettingsViewModel(
val adbPathInput = _adbPathInput.asStateFlow()
val needsAdbSetup = initialSetupStateHolder.needsAdbSetup
val uiState = fontSizeMultiplierUseCase().map {
SettingsUiState(fontSizeMultiplier = it)
}
.stateIn(
viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = SettingsUiState(fontSizeMultiplier = 1f)
)
init {
viewModelScope.launch {
// Utiliser GlobalScope ici pour la simplicité de l'exemple, mais préférez un scope dédié
@ -32,7 +48,20 @@ class SettingsViewModel(
}
}
fun onAction(action: SettingsAction) {
when (action) {
is SettingsAction.FontSizeMultiplierChange -> onFontSizeMultiplierChange(action)
}
}
private fun onFontSizeMultiplierChange(action: SettingsAction.FontSizeMultiplierChange) {
viewModelScope.launch {
setFontSizeMultiplierUseCase(action.value)
}
}
fun onAdbPathChanged(newPath: String) {
_adbPathInput.value = newPath
}