diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 075a33e..27afc44 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -144,4 +144,7 @@ dependencies { // Api implementation(project(":backend-api")) + + // Preferences + implementation(libs.ksprefs) } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/ApiClient.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/ApiClient.kt index 25ef513..2f78228 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/ApiClient.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/ApiClient.kt @@ -2,7 +2,10 @@ package com.github.ai.simplesplit.android.data.api import arrow.core.Either import arrow.core.Some +import com.github.ai.simplesplit.android.data.json.JsonSerializer +import com.github.ai.simplesplit.android.data.settings.Settings import com.github.ai.simplesplit.android.model.exception.ApiException +import com.github.ai.simplesplit.android.utils.atomicReference import com.github.ai.split.api.request.PostExpenseRequest import com.github.ai.split.api.request.PostGroupRequest import com.github.ai.split.api.request.PostMemberRequest @@ -16,13 +19,31 @@ import com.github.ai.split.api.response.PostGroupResponse import com.github.ai.split.api.response.PostMemberResponse import com.github.ai.split.api.response.PutExpenseResponse import com.github.ai.split.api.response.PutGroupResponse -import io.ktor.client.HttpClient class ApiClient( - private val httpClient: HttpClient, - private val baseUrl: String = SERVER_URL + private val jsonSerializer: JsonSerializer, + private val settings: Settings ) { + private var baseUrl by atomicReference(settings.serverUrl) + private var httpClient by atomicReference( + HttpClientFactory.createHttpClient( + jsonSerializer = jsonSerializer, + isSslVerificationEnabled = settings.isSslVerificationEnabled + ) + ) + + fun updateHttpClient() { + httpClient = HttpClientFactory.createHttpClient( + jsonSerializer = jsonSerializer, + isSslVerificationEnabled = settings.isSslVerificationEnabled + ) + } + + fun updateServerUrl() { + baseUrl = settings.serverUrl + } + suspend fun getGroups( uids: List, passwords: List @@ -104,6 +125,7 @@ class ApiClient( ) companion object { - const val SERVER_URL = "https://api.simplesplitapp.link" + const val PROD_SERVER_URL = "https://api.simplesplitapp.link" + const val DEBUG_SERVER_URL = "http://10.0.2.2:8080" } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/data/settings/SettingKey.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/data/settings/SettingKey.kt new file mode 100644 index 0000000..823c1b2 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/data/settings/SettingKey.kt @@ -0,0 +1,12 @@ +package com.github.ai.simplesplit.android.data.settings + +enum class SettingKey( + val key: String +) { + IS_SSL_VERIFICATION_ENABLE( + key = "isSslVerificationEnabled" + ), + SERVER_URL( + key = "serverUrl" + ) +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/data/settings/Settings.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/data/settings/Settings.kt new file mode 100644 index 0000000..2bdac8d --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/data/settings/Settings.kt @@ -0,0 +1,31 @@ +package com.github.ai.simplesplit.android.data.settings + +import android.content.Context +import com.cioccarellia.ksprefs.KsPrefs +import com.github.ai.simplesplit.android.data.api.ApiClient +import com.github.ai.simplesplit.android.data.settings.SettingKey.IS_SSL_VERIFICATION_ENABLE +import com.github.ai.simplesplit.android.data.settings.SettingKey.SERVER_URL + +interface Settings { + var serverUrl: String + var isSslVerificationEnabled: Boolean +} + +class SettingsImpl( + context: Context +) : Settings { + + private val prefs = KsPrefs(context.applicationContext) + + override var isSslVerificationEnabled: Boolean + get() = prefs.pull(IS_SSL_VERIFICATION_ENABLE.key, false) + set(value) { + prefs.push(IS_SSL_VERIFICATION_ENABLE.key, value) + } + + override var serverUrl: String + get() = prefs.pull(SERVER_URL.key, ApiClient.PROD_SERVER_URL) + set(value) { + prefs.push(SERVER_URL.key, value) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/di/AndroidAppModule.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/di/AndroidAppModule.kt index 5d8319f..8641e9f 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/di/AndroidAppModule.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/di/AndroidAppModule.kt @@ -1,13 +1,14 @@ package com.github.ai.simplesplit.android.di import com.github.ai.simplesplit.android.data.api.ApiClient -import com.github.ai.simplesplit.android.data.api.HttpClientFactory import com.github.ai.simplesplit.android.data.database.AppDatabase import com.github.ai.simplesplit.android.data.json.JsonSerializer import com.github.ai.simplesplit.android.data.repository.ExpenseRepository import com.github.ai.simplesplit.android.data.repository.GroupCredentialsRepository import com.github.ai.simplesplit.android.data.repository.GroupRepository import com.github.ai.simplesplit.android.data.repository.MemberRepository +import com.github.ai.simplesplit.android.data.settings.Settings +import com.github.ai.simplesplit.android.data.settings.SettingsImpl import com.github.ai.simplesplit.android.domain.usecase.CreateExportUrlUseCase import com.github.ai.simplesplit.android.domain.usecase.CreateGroupUrlUseCase import com.github.ai.simplesplit.android.domain.usecase.ParseGroupUrlUseCase @@ -40,6 +41,9 @@ import com.github.ai.simplesplit.android.presentation.screens.groupEditor.model. import com.github.ai.simplesplit.android.presentation.screens.groups.GroupsInteractor import com.github.ai.simplesplit.android.presentation.screens.groups.GroupsViewModel import com.github.ai.simplesplit.android.presentation.screens.root.RootViewModel +import com.github.ai.simplesplit.android.presentation.screens.settings.SettingsInteractor +import com.github.ai.simplesplit.android.presentation.screens.settings.SettingsViewModel +import com.github.ai.simplesplit.android.presentation.screens.settings.cells.SettingsCellFactory import org.koin.core.module.dsl.singleOf import org.koin.dsl.bind import org.koin.dsl.module @@ -50,6 +54,7 @@ object AndroidAppModule { // Core singleOf(::ThemeProviderImpl).bind(ThemeProvider::class) singleOf(::ResourceProviderImpl).bind(ResourceProvider::class) + singleOf(::SettingsImpl).bind(Settings::class) // Database single { AppDatabase.buildDatabase(get()) } @@ -57,8 +62,7 @@ object AndroidAppModule { // Api singleOf(::JsonSerializer) - single { HttpClientFactory.createHttpClient(get(), isSslVerificationEnabled = true) } - single { ApiClient(get()) } + singleOf(::ApiClient) // Repositories singleOf(::GroupRepository) @@ -77,10 +81,12 @@ object AndroidAppModule { singleOf(::GroupEditorInteractor) singleOf(::ExpenseEditorInteractor) singleOf(::CheckoutGroupInteractor) + singleOf(::SettingsInteractor) // CellFactories singleOf(::GroupDetailsCellFactory) singleOf(::ExpenseDetailsDialogCellFactory) + singleOf(::SettingsCellFactory) // Router singleOf(::RouterImpl).bind(Router::class) @@ -94,6 +100,14 @@ object AndroidAppModule { get() ) } + factory { + SettingsViewModel( + get(), + get(), + get(), + get() + ) + } factory { (args: GroupDetailsArgs) -> GroupDetailsViewModel( get(), diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateExportUrlUseCase.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateExportUrlUseCase.kt index b23942c..63f53c1 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateExportUrlUseCase.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateExportUrlUseCase.kt @@ -1,13 +1,15 @@ package com.github.ai.simplesplit.android.domain.usecase -import com.github.ai.simplesplit.android.data.api.ApiClient +import com.github.ai.simplesplit.android.data.settings.Settings import com.github.ai.simplesplit.android.model.db.GroupCredentials -class CreateExportUrlUseCase { +class CreateExportUrlUseCase( + private val settings: Settings +) { fun createUrl(credentials: GroupCredentials): String { return "%s/export/%s.csv?password=%s".format( - ApiClient.SERVER_URL, + settings.serverUrl, credentials.groupUid, credentials.password ) diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateGroupUrlUseCase.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateGroupUrlUseCase.kt index 9675e60..36fd1ef 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateGroupUrlUseCase.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/domain/usecase/CreateGroupUrlUseCase.kt @@ -1,13 +1,15 @@ package com.github.ai.simplesplit.android.domain.usecase -import com.github.ai.simplesplit.android.data.api.ApiClient +import com.github.ai.simplesplit.android.data.settings.Settings import com.github.ai.simplesplit.android.model.db.GroupCredentials -class CreateGroupUrlUseCase { +class CreateGroupUrlUseCase( + private val settings: Settings +) { fun createUrl(credentials: GroupCredentials): String { return "%s/group?ids=%s&passwords=%s".format( - ApiClient.SERVER_URL, + settings.serverUrl, credentials.groupUid, credentials.password ) diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/AppDropdownField.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/AppDropdownField.kt index 2198038..1a49f46 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/AppDropdownField.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/AppDropdownField.kt @@ -20,7 +20,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.github.ai.simplesplit.android.presentation.core.compose.preview.ThemedPreview import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon +import com.github.ai.simplesplit.android.presentation.core.compose.theme.ElementMargin import com.github.ai.simplesplit.android.presentation.core.compose.theme.LightTheme +import com.github.ai.simplesplit.android.presentation.core.compose.theme.MediumMargin @Composable fun AppDropdownField( @@ -47,7 +49,11 @@ fun AppDropdownField( Icon( imageVector = AppIcon.EXPAND_MORE.vector, contentDescription = null, - modifier = Modifier.clickable { expanded = true } + modifier = Modifier + .clickable( + onClick = { expanded = true } + ) + .padding(MediumMargin) ) }, modifier = modifier.clickable { expanded = true } @@ -74,7 +80,10 @@ fun AppDropdownField( text = error.orEmpty(), color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(start = 16.dp, top = 4.dp) + modifier = Modifier + .padding( + start = ElementMargin + ) ) } } diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/TopBar.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/TopBar.kt index edf08db..a2bf4fb 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/TopBar.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/TopBar.kt @@ -13,7 +13,8 @@ import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppThem enum class TopBarMenuItem { DONE, - MENU + MENU, + SETTINGS } @OptIn(ExperimentalMaterial3Api::class) @@ -75,5 +76,6 @@ private fun TopBarMenuItem.getImageVector(): ImageVector { return when (this) { TopBarMenuItem.DONE -> AppIcon.CHECK.vector TopBarMenuItem.MENU -> AppIcon.MENU.vector + TopBarMenuItem.SETTINGS -> AppIcon.SETTINGS.vector } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/MutableCellViewModel.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/MutableCellViewModel.kt new file mode 100644 index 0000000..b385388 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/MutableCellViewModel.kt @@ -0,0 +1,13 @@ +package com.github.ai.simplesplit.android.presentation.core.compose.cells + +import kotlinx.coroutines.flow.MutableStateFlow + +abstract class MutableCellViewModel( + initialModel: T +) : CellViewModel { + + val observableModel = MutableStateFlow(initialModel) + + override val model: T + get() = observableModel.value +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/model/DropDownCellEvent.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/model/DropDownCellEvent.kt new file mode 100644 index 0000000..e945ee5 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/model/DropDownCellEvent.kt @@ -0,0 +1,10 @@ +package com.github.ai.simplesplit.android.presentation.core.compose.cells.model + +import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellEvent + +sealed interface DropDownCellEvent : CellEvent { + data class OnOptionSelect( + val cellId: String, + val selectedOption: String + ) : DropDownCellEvent +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/model/DropDownCellModel.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/model/DropDownCellModel.kt new file mode 100644 index 0000000..e336696 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/model/DropDownCellModel.kt @@ -0,0 +1,12 @@ +package com.github.ai.simplesplit.android.presentation.core.compose.cells.model + +import androidx.compose.runtime.Immutable +import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellModel + +@Immutable +data class DropDownCellModel( + override val id: String, + val title: String, + val options: List, + val selectedOption: String +) : CellModel \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/model/SwitchCellEvent.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/model/SwitchCellEvent.kt new file mode 100644 index 0000000..81c8470 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/model/SwitchCellEvent.kt @@ -0,0 +1,11 @@ +package com.github.ai.simplesplit.android.presentation.core.compose.cells.model + +import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellEvent + +interface SwitchCellEvent : CellEvent { + + data class OnCheckChanged( + val cellId: String, + val isChecked: Boolean + ) : SwitchCellEvent +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/model/SwitchCellModel.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/model/SwitchCellModel.kt new file mode 100644 index 0000000..325331e --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/model/SwitchCellModel.kt @@ -0,0 +1,13 @@ +package com.github.ai.simplesplit.android.presentation.core.compose.cells.model + +import androidx.compose.runtime.Immutable +import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellModel + +@Immutable +data class SwitchCellModel( + override val id: String, + val title: String, + val description: String, + val isChecked: Boolean, + val isEnabled: Boolean +) : CellModel \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/ui/DropDownCell.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/ui/DropDownCell.kt new file mode 100644 index 0000000..2880432 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/ui/DropDownCell.kt @@ -0,0 +1,144 @@ +package com.github.ai.simplesplit.android.presentation.core.compose.cells.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.github.ai.simplesplit.android.presentation.core.compose.TextSize.BODY_MEDIUM +import com.github.ai.simplesplit.android.presentation.core.compose.TextSize.TITLE_MEDIUM +import com.github.ai.simplesplit.android.presentation.core.compose.cells.model.DropDownCellEvent +import com.github.ai.simplesplit.android.presentation.core.compose.cells.model.DropDownCellModel +import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.DropDownCellViewModel +import com.github.ai.simplesplit.android.presentation.core.compose.preview.PreviewEventProvider +import com.github.ai.simplesplit.android.presentation.core.compose.preview.ThemedPreview +import com.github.ai.simplesplit.android.presentation.core.compose.rememberCallback +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppTheme +import com.github.ai.simplesplit.android.presentation.core.compose.theme.ElementMargin +import com.github.ai.simplesplit.android.presentation.core.compose.theme.LightTheme +import com.github.ai.simplesplit.android.presentation.core.compose.theme.QuarterMargin +import com.github.ai.simplesplit.android.presentation.core.compose.theme.TwoLineItemHeight +import com.github.ai.simplesplit.android.presentation.core.compose.toTextStyle + +@Composable +fun DropDownCell(viewModel: DropDownCellViewModel) { + val model by viewModel.observableModel.collectAsState() + + var isExpanded by remember { mutableStateOf(false) } + + val onOptionClick = rememberCallback { newOption: String -> + viewModel.sendEvent( + DropDownCellEvent.OnOptionSelect( + cellId = model.id, + selectedOption = newOption + ) + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable( + onClick = { + isExpanded = true + } + ) + .padding( + horizontal = ElementMargin, + vertical = QuarterMargin + ) + .defaultMinSize(minHeight = TwoLineItemHeight) + ) { + Column( + modifier = Modifier + .weight(weight = 1f) + ) { + Text( + text = model.title, + color = AppTheme.theme.colors.primaryText, + style = TITLE_MEDIUM.toTextStyle() + ) + + if (model.selectedOption.isNotEmpty()) { + Text( + text = model.selectedOption, + color = AppTheme.theme.colors.secondaryText, + style = BODY_MEDIUM.toTextStyle() + ) + } + } + + val icon = if (isExpanded) { + AppIcon.ARROW_DROP_UP.vector + } else { + AppIcon.ARROW_DROP_DOWN.vector + } + + Icon( + imageVector = icon, + contentDescription = null, + tint = AppTheme.theme.colors.primaryText + ) + } + + DropdownMenu( + expanded = isExpanded, + onDismissRequest = { isExpanded = false }, + modifier = Modifier + .fillMaxWidth() + ) { + model.options.forEach { option -> + DropdownMenuItem( + text = { Text(option) }, + onClick = { + onOptionClick.invoke(option) + isExpanded = false + } + ) + } + } + } +} + +@Preview +@Composable +fun DropDownCellPreview() { + ThemedPreview( + theme = LightTheme + ) { + Column { + DropDownCell(newDropDownCell()) + } + } +} + +fun newDropDownCell() = + DropDownCellViewModel( + DropDownCellModel( + id = "id", + title = "Title", + options = listOf("Option 1", "Option 2"), + selectedOption = "Option 1" + ), + PreviewEventProvider + ) \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/ui/SwitchCell.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/ui/SwitchCell.kt new file mode 100644 index 0000000..0ddcd8f --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/ui/SwitchCell.kt @@ -0,0 +1,113 @@ +package com.github.ai.simplesplit.android.presentation.core.compose.cells.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Switch +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.tooling.preview.Preview +import com.github.ai.simplesplit.android.presentation.core.compose.cells.model.SwitchCellEvent +import com.github.ai.simplesplit.android.presentation.core.compose.cells.model.SwitchCellModel +import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.SwitchCellViewModel +import com.github.ai.simplesplit.android.presentation.core.compose.preview.ElementSpace +import com.github.ai.simplesplit.android.presentation.core.compose.preview.PreviewEventProvider +import com.github.ai.simplesplit.android.presentation.core.compose.preview.ThemedPreview +import com.github.ai.simplesplit.android.presentation.core.compose.rememberCallback +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppTheme +import com.github.ai.simplesplit.android.presentation.core.compose.theme.ElementMargin +import com.github.ai.simplesplit.android.presentation.core.compose.theme.LightTheme +import com.github.ai.simplesplit.android.presentation.core.compose.theme.QuarterMargin +import com.github.ai.simplesplit.android.presentation.core.compose.theme.SmallMargin +import com.github.ai.simplesplit.android.presentation.core.compose.theme.TwoLineItemHeight + +@Composable +fun SwitchCell(viewModel: SwitchCellViewModel) { + val model by viewModel.observableModel.collectAsState() + + val onChecked = rememberCallback { isChecked: Boolean -> + viewModel.sendIntent( + SwitchCellEvent.OnCheckChanged( + cellId = model.id, + isChecked = isChecked + ) + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = ElementMargin, + vertical = QuarterMargin + ) + .defaultMinSize(minHeight = TwoLineItemHeight) + ) { + Column( + modifier = Modifier + .weight(weight = 1f) + .padding(end = SmallMargin) + ) { + Text( + text = model.title, + color = AppTheme.theme.colors.primaryText, + style = AppTheme.theme.typography.titleMedium + ) + + if (model.description.isNotEmpty()) { + Text( + text = model.description, + color = AppTheme.theme.colors.secondaryText, + style = AppTheme.theme.typography.bodyMedium + ) + } + } + + Switch( + checked = model.isChecked, + enabled = model.isEnabled, + onCheckedChange = onChecked + ) + } +} + +@Composable +@Preview +fun SwitchCellPreview() { + ThemedPreview( + theme = LightTheme + ) { + Column { + SwitchCell(newSwitchCell(isChecked = true, isEnabled = true)) + ElementSpace() + SwitchCell(newSwitchCell(isChecked = true, isEnabled = false)) + ElementSpace() + SwitchCell(newSwitchCell(isChecked = false, isEnabled = true)) + ElementSpace() + SwitchCell(newSwitchCell(isChecked = false, isEnabled = false)) + } + } +} + +fun newSwitchCell( + title: String = "Title", + description: String = "Description", + isChecked: Boolean = false, + isEnabled: Boolean = true +) = SwitchCellViewModel( + model = SwitchCellModel( + id = "id", + title = title, + description = description, + isChecked = isChecked, + isEnabled = isEnabled + ), + eventProvider = PreviewEventProvider +) \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/viewModel/DropDownCellViewModel.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/viewModel/DropDownCellViewModel.kt new file mode 100644 index 0000000..650b49a --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/viewModel/DropDownCellViewModel.kt @@ -0,0 +1,29 @@ +package com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel + +import androidx.compose.runtime.Stable +import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellEventProvider +import com.github.ai.simplesplit.android.presentation.core.compose.cells.MutableCellViewModel +import com.github.ai.simplesplit.android.presentation.core.compose.cells.model.DropDownCellEvent +import com.github.ai.simplesplit.android.presentation.core.compose.cells.model.DropDownCellModel + +@Stable +class DropDownCellViewModel( + initialModel: DropDownCellModel, + private val eventProvider: CellEventProvider +) : MutableCellViewModel(initialModel) { + + fun sendEvent(event: DropDownCellEvent) { + handleEvent(event) + eventProvider.sendEvent(event) + } + + private fun handleEvent(event: DropDownCellEvent) { + when (event) { + is DropDownCellEvent.OnOptionSelect -> { + observableModel.value = observableModel.value.copy( + selectedOption = event.selectedOption + ) + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/viewModel/SwitchCellViewModel.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/viewModel/SwitchCellViewModel.kt new file mode 100644 index 0000000..b1ee104 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/cells/viewModel/SwitchCellViewModel.kt @@ -0,0 +1,29 @@ +package com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel + +import androidx.compose.runtime.Stable +import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellEventProvider +import com.github.ai.simplesplit.android.presentation.core.compose.cells.MutableCellViewModel +import com.github.ai.simplesplit.android.presentation.core.compose.cells.model.SwitchCellEvent +import com.github.ai.simplesplit.android.presentation.core.compose.cells.model.SwitchCellModel + +@Stable +class SwitchCellViewModel( + model: SwitchCellModel, + private val eventProvider: CellEventProvider +) : MutableCellViewModel(model) { + + fun sendIntent(event: SwitchCellEvent) { + handleEvent(event) + eventProvider.sendEvent(event) + } + + private fun handleEvent(event: SwitchCellEvent) { + when (event) { + is SwitchCellEvent.OnCheckChanged -> { + observableModel.value = observableModel.value.copy( + isChecked = event.isChecked + ) + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/AppIcon.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/AppIcon.kt index 4e6e7d1..137f233 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/AppIcon.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/AppIcon.kt @@ -4,6 +4,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBackIos import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.AddLink +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowDropUp import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit @@ -33,7 +35,9 @@ enum class AppIcon { EDIT, REMOVE, EXPORT, - SHARE; + SHARE, + ARROW_DROP_DOWN, + ARROW_DROP_UP; val vector: ImageVector get() = @@ -53,5 +57,7 @@ enum class AppIcon { REMOVE -> Icons.Filled.Delete EXPORT -> Icons.Filled.FileUpload SHARE -> Icons.Filled.Share + ARROW_DROP_DOWN -> Icons.Filled.ArrowDropDown + ARROW_DROP_UP -> Icons.Filled.ArrowDropUp } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/Dimens.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/Dimens.kt index 13c5947..b8cd894 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/Dimens.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/core/compose/theme/Dimens.kt @@ -15,6 +15,7 @@ val HugeMargin = 64.dp val EmptyMessageItemHeight = 200.dp val OneLineItemHeight = 48.dp +val TwoLineItemHeight = 72.dp val CardCornerSize = 22.dp val DialogCardCornerSize = 16.dp \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/Screen.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/Screen.kt index 6dab496..379948e 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/Screen.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/Screen.kt @@ -13,6 +13,9 @@ sealed interface Screen : ResultOwner { @Serializable data object Groups : Screen + @Serializable + data object Settings : Screen + @Serializable data class GroupDetails( val args: GroupDetailsArgs diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsScreen.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsScreen.kt index e4e93be..2fb6642 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsScreen.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsScreen.kt @@ -15,12 +15,15 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import com.github.ai.simplesplit.android.BuildConfig import com.github.ai.simplesplit.android.R import com.github.ai.simplesplit.android.presentation.core.compose.CenteredBox import com.github.ai.simplesplit.android.presentation.core.compose.TopBar +import com.github.ai.simplesplit.android.presentation.core.compose.TopBarMenuItem import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellViewModel import com.github.ai.simplesplit.android.presentation.core.compose.cells.ui.SpaceCell import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.SpaceCellViewModel +import com.github.ai.simplesplit.android.presentation.core.compose.rememberCallback import com.github.ai.simplesplit.android.presentation.core.compose.rememberOnClickedCallback import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppIcon import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppTheme @@ -47,12 +50,21 @@ private fun GroupsScreen( val onFabClick = rememberOnClickedCallback { onIntent.invoke(GroupsIntent.OnAddButtonClick) } + val onMenuClick = rememberCallback { item: TopBarMenuItem -> + onIntent.invoke(GroupsIntent.OnSettingsClick) + } Scaffold( topBar = { TopBar( title = stringResource(R.string.groups), - isBackVisible = false + isBackVisible = false, + menuItems = if (BuildConfig.DEBUG) { + listOf(TopBarMenuItem.SETTINGS) + } else { + emptyList() + }, + onMenuItemClick = onMenuClick ) }, floatingActionButton = { @@ -68,7 +80,6 @@ private fun GroupsScreen( } } ) { padding -> - Surface( modifier = Modifier .fillMaxSize() @@ -116,5 +127,6 @@ private fun RenderCell(viewModel: CellViewModel) { when (viewModel) { is SpaceCellViewModel -> SpaceCell(viewModel) is GroupCellViewModel -> GroupCell(viewModel) + else -> throw IllegalArgumentException("Unknown cell: $viewModel") } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsViewModel.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsViewModel.kt index 61293e7..9596d5f 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsViewModel.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/GroupsViewModel.kt @@ -99,6 +99,9 @@ class GroupsViewModel( is GroupsIntent.ShareUrl -> nonStateAction { router.startActivity(StartActivityEvent.ShareUrl(intent.url)) } + + GroupsIntent.OnSettingsClick -> + nonStateAction { navigateToSettingsScreen() } } } @@ -203,6 +206,10 @@ class GroupsViewModel( ) } + private fun navigateToSettingsScreen() { + router.navigateTo(Screen.Settings) + } + private fun showAddGroupMenuDialog() { router.showDialog( Dialog.MenuDialog( diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/cells/ui/GroupCell.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/cells/ui/GroupCell.kt index d7f5526..1643c98 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/cells/ui/GroupCell.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/cells/ui/GroupCell.kt @@ -116,7 +116,7 @@ fun GroupCellPreview() { } } -private fun newGroupCell() = +fun newGroupCell() = GroupCellViewModel( model = GroupCellModel( id = "id", diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/model/GroupsIntent.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/model/GroupsIntent.kt index 3c78c64..1e32022 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/model/GroupsIntent.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/groups/model/GroupsIntent.kt @@ -11,6 +11,7 @@ sealed class GroupsIntent( data object OnAddButtonClick : GroupsIntent() data object OnCreateGroupClick : GroupsIntent() data object OnAddGroupByUrlClick : GroupsIntent() + data object OnSettingsClick : GroupsIntent() data class OnGroupClick(val groupUid: String) : GroupsIntent() data class OnGroupLongClick(val groupUid: String) : GroupsIntent() data class OnEditGroupClick(val groupUid: String) : GroupsIntent() diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/RootScreenComponent.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/RootScreenComponent.kt index 927849c..3c8e8b1 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/RootScreenComponent.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/root/RootScreenComponent.kt @@ -15,6 +15,7 @@ import com.github.ai.simplesplit.android.presentation.screens.groupDetails.Group import com.github.ai.simplesplit.android.presentation.screens.groupEditor.GroupEditorScreenComponent import com.github.ai.simplesplit.android.presentation.screens.groups.GroupsScreenComponent import com.github.ai.simplesplit.android.presentation.screens.root.model.RootIntent +import com.github.ai.simplesplit.android.presentation.screens.settings.SettingsScreenComponent class RootScreenComponent( componentContext: ComponentContext @@ -57,6 +58,10 @@ class RootScreenComponent( context = childContext ) + is Screen.Settings -> SettingsScreenComponent( + context = childContext + ) + is Screen.GroupDetails -> GroupDetailsScreenComponent( context = childContext, args = screen.args diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/SettingsInteractor.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/SettingsInteractor.kt new file mode 100644 index 0000000..f7b4a12 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/SettingsInteractor.kt @@ -0,0 +1,16 @@ +package com.github.ai.simplesplit.android.presentation.screens.settings + +import com.github.ai.simplesplit.android.data.api.ApiClient + +class SettingsInteractor( + private val api: ApiClient +) { + + fun onSslVerificationEnabledChanged() { + api.updateHttpClient() + } + + fun onServerUrlChanged() { + api.updateServerUrl() + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/SettingsScreen.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/SettingsScreen.kt new file mode 100644 index 0000000..9eef07c --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/SettingsScreen.kt @@ -0,0 +1,102 @@ +package com.github.ai.simplesplit.android.presentation.screens.settings + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.github.ai.simplesplit.android.R +import com.github.ai.simplesplit.android.presentation.core.compose.CenteredBox +import com.github.ai.simplesplit.android.presentation.core.compose.TopBar +import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellViewModel +import com.github.ai.simplesplit.android.presentation.core.compose.cells.ui.DropDownCell +import com.github.ai.simplesplit.android.presentation.core.compose.cells.ui.SpaceCell +import com.github.ai.simplesplit.android.presentation.core.compose.cells.ui.SwitchCell +import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.DropDownCellViewModel +import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.SpaceCellViewModel +import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.SwitchCellViewModel +import com.github.ai.simplesplit.android.presentation.core.compose.rememberOnClickedCallback +import com.github.ai.simplesplit.android.presentation.core.compose.theme.AppTheme +import com.github.ai.simplesplit.android.presentation.screens.settings.model.SettingsIntent +import com.github.ai.simplesplit.android.presentation.screens.settings.model.SettingsState + +@Composable +fun SettingsScreen(viewModel: SettingsViewModel) { + val state by viewModel.state.collectAsState() + + SettingsScreen( + state = state, + onIntent = viewModel::sendIntent + ) +} + +@Composable +private fun SettingsScreen( + state: SettingsState, + onIntent: (intent: SettingsIntent) -> Unit +) { + val onBackClick = rememberOnClickedCallback { + onIntent.invoke(SettingsIntent.OnBackClick) + } + + Scaffold( + topBar = { + TopBar( + title = stringResource(R.string.settings), + isBackVisible = true, + onBackClick = onBackClick + ) + } + ) { padding -> + + Surface( + modifier = Modifier + .fillMaxSize() + .padding( + top = padding.calculateTopPadding(), + bottom = padding.calculateBottomPadding() + ), + color = AppTheme.theme.colors.background + ) { + when (state) { + SettingsState.Loading -> { + CenteredBox { CircularProgressIndicator() } + } + + is SettingsState.Data -> { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding() + ) { + items(state.cellViewModels) { model -> + RenderCell(model) + } + } + } + + is SettingsState.Error -> { + CenteredBox { Text(text = state.message) } + } + } + } + } +} + +@Composable +private fun RenderCell(viewModel: CellViewModel) { + when (viewModel) { + is SpaceCellViewModel -> SpaceCell(viewModel) + is SwitchCellViewModel -> SwitchCell(viewModel) + is DropDownCellViewModel -> DropDownCell(viewModel) + else -> throw IllegalArgumentException("Unknown cell: $viewModel") + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/SettingsScreenComponent.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/SettingsScreenComponent.kt new file mode 100644 index 0000000..f093e40 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/SettingsScreenComponent.kt @@ -0,0 +1,33 @@ +package com.github.ai.simplesplit.android.presentation.screens.settings + +import androidx.compose.runtime.Composable +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import com.arkivanov.decompose.ComponentContext +import com.github.ai.simplesplit.android.presentation.core.ViewModelFactory +import com.github.ai.simplesplit.android.presentation.core.ViewModelStoreOwnerImpl +import com.github.ai.simplesplit.android.presentation.core.compose.navigation.ScreenComponent +import com.github.ai.simplesplit.android.presentation.core.mvi.attach + +class SettingsScreenComponent( + context: ComponentContext +) : ScreenComponent, + ComponentContext by context, + ViewModelStoreOwner by ViewModelStoreOwnerImpl() { + + private val viewModel: SettingsViewModel by lazy { + ViewModelProvider( + owner = this, + factory = ViewModelFactory() + )[SettingsViewModel::class] + } + + init { + lifecycle.attach(viewModel) + } + + @Composable + override fun render() { + SettingsScreen(viewModel) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/SettingsViewModel.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/SettingsViewModel.kt new file mode 100644 index 0000000..07f2703 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/SettingsViewModel.kt @@ -0,0 +1,60 @@ +package com.github.ai.simplesplit.android.presentation.screens.settings + +import com.github.ai.simplesplit.android.data.settings.Settings +import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellEvent +import com.github.ai.simplesplit.android.presentation.core.compose.cells.model.DropDownCellEvent +import com.github.ai.simplesplit.android.presentation.core.compose.cells.model.SwitchCellEvent +import com.github.ai.simplesplit.android.presentation.core.compose.navigation.Router +import com.github.ai.simplesplit.android.presentation.core.mvi.CellsMviViewModel +import com.github.ai.simplesplit.android.presentation.core.mvi.nonStateAction +import com.github.ai.simplesplit.android.presentation.screens.settings.cells.SettingsCellFactory +import com.github.ai.simplesplit.android.presentation.screens.settings.model.SettingsIntent +import com.github.ai.simplesplit.android.presentation.screens.settings.model.SettingsState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class SettingsViewModel( + private val interactor: SettingsInteractor, + private val settings: Settings, + private val cellFactory: SettingsCellFactory, + private val router: Router +) : CellsMviViewModel( + initialState = SettingsState.Loading, + initialIntent = SettingsIntent.Initialize +) { + + override fun handleIntent(intent: SettingsIntent): Flow { + return when (intent) { + SettingsIntent.Initialize -> loadData() + SettingsIntent.OnBackClick -> nonStateAction { navigateBack() } + } + } + + private fun loadData(): Flow = + flowOf( + SettingsState.Data( + cellViewModels = cellFactory.createCells( + settings = settings, + eventProvider = cellEventProvider + ) + ) + ) + + override fun handleCellEvent(event: CellEvent) { + when (event) { + is SwitchCellEvent.OnCheckChanged -> { + settings.isSslVerificationEnabled = event.isChecked + interactor.onSslVerificationEnabledChanged() + } + + is DropDownCellEvent.OnOptionSelect -> { + settings.serverUrl = event.selectedOption + interactor.onServerUrlChanged() + } + } + } + + private fun navigateBack() { + router.exit() + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/cells/SettingsCellFactory.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/cells/SettingsCellFactory.kt new file mode 100644 index 0000000..c7bc0ad --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/cells/SettingsCellFactory.kt @@ -0,0 +1,58 @@ +package com.github.ai.simplesplit.android.presentation.screens.settings.cells + +import com.github.ai.simplesplit.android.R +import com.github.ai.simplesplit.android.data.api.ApiClient +import com.github.ai.simplesplit.android.data.settings.Settings +import com.github.ai.simplesplit.android.presentation.core.ResourceProvider +import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellEventProvider +import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellViewModel +import com.github.ai.simplesplit.android.presentation.core.compose.cells.model.DropDownCellModel +import com.github.ai.simplesplit.android.presentation.core.compose.cells.model.SwitchCellModel +import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.DropDownCellViewModel +import com.github.ai.simplesplit.android.presentation.core.compose.cells.viewModel.SwitchCellViewModel + +class SettingsCellFactory( + private val resources: ResourceProvider +) { + + fun createCells( + settings: Settings, + eventProvider: CellEventProvider + ): List { + val cells = mutableListOf() + + cells.add( + DropDownCellViewModel( + DropDownCellModel( + id = SettingsCellId.SERVER_URL.name, + title = resources.getString(R.string.server_url), + options = listOf(ApiClient.PROD_SERVER_URL, ApiClient.DEBUG_SERVER_URL), + selectedOption = settings.serverUrl + ), + eventProvider + ) + ) + + cells.add( + SwitchCellViewModel( + SwitchCellModel( + id = SettingsCellId.SSL_CERTIFICATE_SWITCH.name, + title = resources.getString(R.string.validate_ssl_certificate_title), + description = resources.getString( + R.string.validate_ssl_certificate_description + ), + isChecked = settings.isSslVerificationEnabled, + isEnabled = true + ), + eventProvider + ) + ) + + return cells + } + + enum class SettingsCellId { + SERVER_URL, + SSL_CERTIFICATE_SWITCH + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/model/SettingsIntent.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/model/SettingsIntent.kt new file mode 100644 index 0000000..210fb7c --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/model/SettingsIntent.kt @@ -0,0 +1,10 @@ +package com.github.ai.simplesplit.android.presentation.screens.settings.model + +import com.github.ai.simplesplit.android.presentation.core.mvi.MviIntent + +sealed class SettingsIntent( + override val isImmediate: Boolean = false +) : MviIntent { + data object Initialize : SettingsIntent() + data object OnBackClick : SettingsIntent() +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/model/SettingsState.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/model/SettingsState.kt new file mode 100644 index 0000000..42389d5 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/presentation/screens/settings/model/SettingsState.kt @@ -0,0 +1,9 @@ +package com.github.ai.simplesplit.android.presentation.screens.settings.model + +import com.github.ai.simplesplit.android.presentation.core.compose.cells.CellViewModel + +sealed class SettingsState { + data object Loading : SettingsState() + data class Error(val message: String) : SettingsState() + data class Data(val cellViewModels: List) : SettingsState() +} \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/utils/AtomicReferenceDelegate.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/utils/AtomicReferenceDelegate.kt new file mode 100644 index 0000000..5811078 --- /dev/null +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/utils/AtomicReferenceDelegate.kt @@ -0,0 +1,23 @@ +package com.github.ai.simplesplit.android.utils + +import java.util.concurrent.atomic.AtomicReference +import kotlin.reflect.KProperty + +class AtomicReferenceDelegate( + private val reference: AtomicReference +) { + + operator fun getValue( + ref: Any?, + property: KProperty<*> + ): T = reference.get() + + operator fun setValue( + ref: Any?, + property: KProperty<*>, + value: T + ) = reference.set(value) +} + +fun atomicReference(initialValue: T): AtomicReferenceDelegate = + AtomicReferenceDelegate(AtomicReference(initialValue)) \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 9ed750f..09666f4 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -17,6 +17,12 @@ Create new group Add by url + + Settings + Server URL + Validate SSL certificates + Disables SSL certificate validation during HTTPS requests + Create New Group Group Name diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index c8ddbef..be849b5 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -39,6 +39,7 @@ dadb = "1.2.8" mordant = "3.0.1" truth = "1.4.4" jgit = "6.2.0.202206071550-r" +ksprefs = "2.4.1" [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } @@ -100,7 +101,7 @@ logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } room-test = { module = "androidx.room:room-testing", version.ref = "room" } -room-kts = { module = "androidx.room:room-ktx", version.ref = "room"} +room-kts = { module = "androidx.room:room-ktx", version.ref = "room" } jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } jackson-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" } arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" } @@ -115,4 +116,5 @@ coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" } dadb = { module = "dev.mobile:dadb", version.ref = "dadb" } mordant = { module = "com.github.ajalt.mordant:mordant", version.ref = "mordant" } truth = { module = "com.google.truth:truth", version.ref = "truth" } -jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" } \ No newline at end of file +jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" } +ksprefs = { module = "com.github.cioccarellia:ksprefs", version.ref = "ksprefs" } \ No newline at end of file