diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 565efcd..045759b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ diff --git a/buildSrc/src/main/java/BuildValues.kt b/buildSrc/src/main/java/BuildValues.kt index 21a910a..5da619b 100644 --- a/buildSrc/src/main/java/BuildValues.kt +++ b/buildSrc/src/main/java/BuildValues.kt @@ -4,11 +4,11 @@ import org.gradle.api.JavaVersion * Created by MD on 18.10.23. */ object BuildValues { - val minSdk = 23 - val targetSdk = 33 - val compileSdk = 33 + const val minSdk = 23 + const val targetSdk = 33 + const val compileSdk = 33 val javaVersion : JavaVersion = JavaVersion.VERSION_1_8 - val jvmTarget = "1.8" - val kotlinCompilerExtensionVersion = "1.2.0" + const val jvmTarget = "1.8" + const val kotlinCompilerExtensionVersion = "1.2.0" } \ No newline at end of file diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 45a922f..a1eae66 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -2,54 +2,55 @@ * Created by MD on 18.10.23. */ object Versions { - val composeVersion = "1.1.1" - val koinVersion = "3.4.0" - val roomVersion = "2.5.1" + const val composeVersion = "1.1.1" + const val koinVersion = "3.4.0" + const val roomVersion = "2.5.1" } object Dependencies { - val core_ktx = "androidx.core:core-ktx:1.10.0" - val activity_compose = "androidx.activity:activity-compose:1.7.1" + const val core_ktx = "androidx.core:core-ktx:1.10.0" + const val activity_compose = "androidx.activity:activity-compose:1.7.1" - val lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1" + const val lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1" /** * LIFECYCLE AWARE FLOW COLLECTION (for lifecycle aware viewstate collection in compose) */ - val lifecycle_runtime_compose = "androidx.lifecycle:lifecycle-runtime-compose:2.6.1" + const val lifecycle_runtime_compose = "androidx.lifecycle:lifecycle-runtime-compose:2.6.1" // COMPOSE - val compose_ui = "androidx.compose.ui:ui:${Versions.composeVersion}" + const val compose_ui = "androidx.compose.ui:ui:${Versions.composeVersion}" // val compose_ui_tooling_preview = "androidx.compose.ui:ui-tooling-preview:${Versions.composeVersion}" - val compose_ui_tooling_preview = "androidx.compose.ui:ui-tooling-preview:1.4.2" - val compose_material = "androidx.compose.material:material:${Versions.composeVersion}" - val compose_ui_tooling = "androidx.compose.ui:ui-tooling:${Versions.composeVersion}" - val compose_ui_test_manifest = "androidx.compose.ui:ui-test-manifest:${Versions.composeVersion}" - val compose_navigation = "androidx.navigation:navigation-compose:2.5.3" - val compose_ui_test_junit = "androidx.compose.ui:ui-test-junit4:${Versions.composeVersion}" + + const val compose_ui_tooling_preview = "androidx.compose.ui:ui-tooling-preview:1.4.2" + const val compose_material = "androidx.compose.material:material:${Versions.composeVersion}" + const val compose_ui_tooling = "androidx.compose.ui:ui-tooling:${Versions.composeVersion}" + const val compose_ui_test_manifest = "androidx.compose.ui:ui-test-manifest:${Versions.composeVersion}" + const val compose_navigation = "androidx.navigation:navigation-compose:2.5.3" + const val compose_ui_test_junit = "androidx.compose.ui:ui-test-junit4:${Versions.composeVersion}" /** * IMMUTABLE COLLECTIONS FOR KOTLIN (for composable methods stability) */ - val collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5" + const val collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5" // KOIN - val koin_core = "io.insert-koin:koin-core:${Versions.koinVersion}" - val koin_navigation = "io.insert-koin:koin-androidx-navigation:${Versions.koinVersion}" - val koin_compose = "io.insert-koin:koin-androidx-compose:${Versions.koinVersion}" - val koin_junit = "io.insert-koin:koin-test-junit4:${Versions.koinVersion}" + const val koin_core = "io.insert-koin:koin-core:${Versions.koinVersion}" + const val koin_navigation = "io.insert-koin:koin-androidx-navigation:${Versions.koinVersion}" + const val koin_compose = "io.insert-koin:koin-androidx-compose:${Versions.koinVersion}" + const val koin_junit = "io.insert-koin:koin-test-junit4:${Versions.koinVersion}" // ROOM - val room_runtime = "androidx.room:room-runtime:${Versions.roomVersion}" - val room_compiler = "androidx.room:room-compiler:${Versions.roomVersion}" - val room_room = "androidx.room:room-ktx:${Versions.roomVersion}" + const val room_runtime = "androidx.room:room-runtime:${Versions.roomVersion}" + const val room_compiler = "androidx.room:room-compiler:${Versions.roomVersion}" + const val room_room = "androidx.room:room-ktx:${Versions.roomVersion}" /** * DESUGARING (e.g. for LocalDate manipulation on API < 26) */ - val desugar = "com.android.tools:desugar_jdk_libs:1.2.2" + const val desugar = "com.android.tools:desugar_jdk_libs:1.2.2" // TESTS - val test_junit = "junit:junit:4.13.2" - val test_ext_junit = "androidx.test.ext:junit:1.1.5" - val test_espresso = "androidx.test.espresso:espresso-core:3.5.1" + const val test_junit = "junit:junit:4.13.2" + const val test_ext_junit = "androidx.test.ext:junit:1.1.5" + const val test_espresso = "androidx.test.espresso:espresso-core:3.5.1" } \ No newline at end of file diff --git a/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/base/BaseViewModel.kt b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/base/BaseViewModel.kt index d98dcef..35055a7 100644 --- a/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/base/BaseViewModel.kt +++ b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/base/BaseViewModel.kt @@ -7,17 +7,16 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch /** * Created by MD on 29.12.22. */ -abstract class BaseViewModel(val initialState: STATE) : ViewModel() { +abstract class BaseViewModel(val initialState: STATE) : ViewModel() { // todo - use explicit backing fields when project is switched to Kotlin 2.0, see https://github.com/Kotlin/KEEP/blob/explicit-backing-fields-re/proposals/explicit-backing-fields.md @@ -29,8 +28,8 @@ abstract class BaseViewModel(Channel.UNLIMITED) // For handling commands from VM to UI - private val _commandFlow = MutableSharedFlow() - val commandFlow: Flow = _commandFlow.asSharedFlow() + private val commandChannel = Channel() + val commandFlow: Flow = commandChannel.receiveAsFlow() /** * This is the job for all coroutines started by this ViewModel. @@ -89,8 +88,10 @@ abstract class BaseViewModel Unit, +) { + Button( + modifier = modifier + .heightIn(min = 48.dp), + enabled = enabled, + shape = MaterialTheme.shapes.medium, + onClick = onClick, + ) { + Text(text = text) + } +} \ No newline at end of file diff --git a/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/CardThemed.kt b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/TBCCard.kt similarity index 98% rename from commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/CardThemed.kt rename to commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/TBCCard.kt index c11cc9b..1d0a127 100644 --- a/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/CardThemed.kt +++ b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/TBCCard.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.unit.dp * Created by MD on 28.04.23. */ @Composable -fun CardThemed( +fun TBCCard( modifier: Modifier = Modifier, backgroundColor: Color = MaterialTheme.colors.surface, borderColor: Color? = null, diff --git a/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/DropdownMenu.kt b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/TBCDropdownMenu.kt similarity index 99% rename from commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/DropdownMenu.kt rename to commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/TBCDropdownMenu.kt index 6db6c1a..b701024 100644 --- a/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/DropdownMenu.kt +++ b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/TBCDropdownMenu.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.launch * e.g. has to be in the same box as the button opening the dropdown */ @Composable -fun DropdownMenu( +fun TBCDropdownMenu( dropdownShown: MutableState, scaffoldState: ScaffoldState?, items: List diff --git a/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/TBCStateWrapper.kt b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/TBCStateWrapper.kt new file mode 100644 index 0000000..428fad2 --- /dev/null +++ b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/TBCStateWrapper.kt @@ -0,0 +1,53 @@ +package cz.damat.thebeercounter.commonUI.compose.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.size +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import cz.damat.thebeercounter.commonUI.base.State + + +/** + * Created by MD on 08.11.23. + */ +@Composable +fun TBCStateWrapper( + modifier: Modifier = Modifier, + state: State, + customLoading: @Composable (BoxScope.() -> Unit)? = null, + customError: @Composable (BoxScope.() -> Unit)? = null, + content: @Composable () -> Unit, +) { + Box(modifier = modifier) { + when (state) { + State.Content -> content() + is State.Error -> customError?.invoke(this) ?: Error(message = stringResource(id = state.message)) + State.Loading -> customLoading?.invoke(this) ?: Loading() + } + } +} + +@Composable +private fun BoxScope.Loading() { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .size(64.dp), + ) +} + +@Composable +private fun BoxScope.Error( + message: String, +) { + Text( + modifier = Modifier.align(Alignment.Center), + text = message + ) +} \ No newline at end of file diff --git a/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/TBCTextField.kt b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/TBCTextField.kt new file mode 100644 index 0000000..46f9b2e --- /dev/null +++ b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/component/TBCTextField.kt @@ -0,0 +1,136 @@ +package cz.damat.thebeercounter.commonUI.compose.component + +import android.view.KeyEvent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.Composable +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.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import cz.damat.thebeercounter.commonUI.compose.utils.applyIf + + +/** + * Created by MD on 09.11.23. + */ +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun TBCTextField( + modifier: Modifier = Modifier, + label: String?, + value: String, + enabled: Boolean = true, + readOnly: Boolean = false, + placeholder: String? = null, + error: String? = null, + singleLine: Boolean = false, + hasNextDownImeAction: Boolean = false, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + onClick: (() -> Unit)? = null, + onValueChanged: (String) -> Unit, +) { + // mutable state that gets updated "twice" is necessary otherwise cursors skips when writing fast + var valueState by remember(value) { + mutableStateOf(value) + } + + val focusManager = LocalFocusManager.current + + val textFieldShape = MaterialTheme.shapes.medium + OutlinedTextField( + modifier = modifier + .clip(textFieldShape) + .applyIf(onClick != null) { + clickable { onClick?.invoke() } + } + .applyIf(hasNextDownImeAction) { + onPreviewKeyEvent { + if (it.key == Key.Tab && it.nativeKeyEvent.action == KeyEvent.ACTION_DOWN) { + focusManager.moveFocus(FocusDirection.Down) + true + } else { + false + } + } + }, + shape = textFieldShape, + value = valueState, + onValueChange = { + valueState = it + onValueChanged(it) + }, + label = { + if (label != null) { + Text( + text = label, + style = MaterialTheme.typography.body2 + ) + } + }, + readOnly = readOnly, + enabled = enabled, + isError = error != null, + singleLine = singleLine, + placeholder = { + Text( + text = placeholder ?: "", + style = MaterialTheme.typography.body2.copy(fontWeight = FontWeight.Normal), + textAlign = TextAlign.Start + ) + }, + textStyle = MaterialTheme.typography.body2.copy(fontWeight = FontWeight.Normal), + keyboardOptions = if (hasNextDownImeAction) keyboardOptions.copy(imeAction = ImeAction.Next) else keyboardOptions, + keyboardActions = if (hasNextDownImeAction) { + KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }) + } else { + keyboardActions + }, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + interactionSource = interactionSource, + ) + + if (error != null) { + Spacer(modifier = Modifier.height(8.dp)) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = error, + style = MaterialTheme.typography.body2.copy(fontWeight = FontWeight.SemiBold), + color = MaterialTheme.colors.error, + ) + } +} \ No newline at end of file diff --git a/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/theme/Type.kt b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/theme/Typography.kt similarity index 61% rename from commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/theme/Type.kt rename to commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/theme/Typography.kt index 7627fdd..6244186 100644 --- a/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/theme/Type.kt +++ b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/theme/Typography.kt @@ -6,22 +6,18 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp + +val fontFamily = FontFamily.Default + val Typography = Typography( body1 = TextStyle( - fontFamily = FontFamily.Default, + fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 16.sp - ) - /* Other default text styles to override - button = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.W500, - fontSize = 14.sp ), - caption = TextStyle( - fontFamily = FontFamily.Default, + body2 = TextStyle( + fontFamily = fontFamily, fontWeight = FontWeight.Normal, - fontSize = 12.sp + fontSize = 14.sp ) - */ ) \ No newline at end of file diff --git a/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/utils/ComposeHelperExt.kt b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/utils/ComposeHelperExt.kt new file mode 100644 index 0000000..98dc333 --- /dev/null +++ b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/compose/utils/ComposeHelperExt.kt @@ -0,0 +1,19 @@ +package cz.damat.thebeercounter.commonUI.compose.utils + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed + + +/** + * Created by MD on 09.11.23. + */ +@SuppressLint("UnnecessaryComposedModifier") // is actually necessary +fun Modifier.applyIf(condition: Boolean, modify: @Composable Modifier.() -> Modifier): Modifier = composed { + if (condition) { + this.modify() + } else { + this + } +} \ No newline at end of file diff --git a/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/utils/MVIHelper.kt b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/utils/MVIHelper.kt index 028d792..eba1583 100644 --- a/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/utils/MVIHelper.kt +++ b/commonUI/src/main/java/cz/damat/thebeercounter/commonUI/utils/MVIHelper.kt @@ -8,8 +8,9 @@ import androidx.lifecycle.flowWithLifecycle import cz.damat.thebeercounter.commonUI.base.BaseViewModel import cz.damat.thebeercounter.commonUI.base.ViewCommand import cz.damat.thebeercounter.commonUI.base.ViewEvent -import cz.damat.thebeercounter.commonUI.base.ViewState +import cz.damat.thebeercounter.commonUI.base.ViewStateDTO import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -25,7 +26,7 @@ import kotlinx.coroutines.plus * Collects the [BaseViewModel.stateFlow] by using [collectAsStateWithLifecycle] and the [BaseViewModel.initialState]. */ @Composable -fun BaseViewModel.collectStateWithLifecycle(): State { +fun BaseViewModel.collectStateWithLifecycle(): State { return stateFlow.collectAsStateWithLifecycle(initialValue = this.initialState) } @@ -58,9 +59,7 @@ fun BaseViewModel<*, *, T>.collectCommand( ) { val lifecycleOwner = LocalLifecycleOwner.current - LaunchedEffect(key1 = Unit) { - // unchanging key makes sure that the collection is started only once - + LaunchedEffect(lifecycleOwner.lifecycle, commandFlow) { commandFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) .onEach(block) // since the command flow in not a StateFlow the block is called only at the time something is posted to the flow. .launchIn(this + baseCoroutineExceptionHandler) diff --git a/commonUI/src/main/res/drawable/ic_remove_24.xml b/commonUI/src/main/res/drawable/ic_remove_24.xml new file mode 100644 index 0000000..45eb7bc --- /dev/null +++ b/commonUI/src/main/res/drawable/ic_remove_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/commonUI/src/main/res/values/strings.xml b/commonUI/src/main/res/values/strings.xml index 5d637fc..75d5d44 100644 --- a/commonUI/src/main/res/values/strings.xml +++ b/commonUI/src/main/res/values/strings.xml @@ -5,18 +5,21 @@ History More + Edit Reset Delete Set count Set Clear all Add + Remove Count Your tab: OK Cancel + Back Clear Expand Collapse @@ -24,4 +27,6 @@ Name Beer Do you really want to delete all items? + Edit product + Product not found \ No newline at end of file diff --git a/componentCounter/src/main/java/cz/damat/thebeercounter/componentCounter/data/repositoryImpl/ProductRepositoryImpl.kt b/componentCounter/src/main/java/cz/damat/thebeercounter/componentCounter/data/repositoryImpl/ProductRepositoryImpl.kt index e97d0f9..8456777 100644 --- a/componentCounter/src/main/java/cz/damat/thebeercounter/componentCounter/data/repositoryImpl/ProductRepositoryImpl.kt +++ b/componentCounter/src/main/java/cz/damat/thebeercounter/componentCounter/data/repositoryImpl/ProductRepositoryImpl.kt @@ -9,7 +9,9 @@ import cz.damat.thebeercounter.commonlib.room.entity.HistoryItemType import cz.damat.thebeercounter.commonlib.room.entity.INITIAL_ITEM_ID import cz.damat.thebeercounter.commonlib.room.entity.Product import cz.damat.thebeercounter.componentCounter.domain.repository.ProductRepository +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext /** @@ -18,6 +20,10 @@ import kotlinx.coroutines.flow.first class ProductRepositoryImpl(private val db: AppDatabase, private val productDao: ProductDao, private val historyItemDao: HistoryItemDao) : ProductRepository { + override suspend fun getProduct(id: Int): Product? { + return productDao.getProduct(id) + } + override fun getShownProductsFlow() = productDao.getProductsFlow(true) //todo - use for allowing the user to delete currently not shown products @@ -64,6 +70,26 @@ class ProductRepositoryImpl(private val db: AppDatabase, private val productDao: } } + override suspend fun updateProductNameAndSetCount(id: Int, name: String, count: Int) { + db.withTransaction { + productDao.getProduct(id)?.let { product -> + val oldCount = product.count + productDao.upsert(product.copy(name = name, count = count)) + + if (oldCount != count) { + historyItemDao.upsert( + HistoryItem( + productId = id, + oldCount = oldCount, + newCount = count, + type = HistoryItemType.MANUAL + ) + ) + } + } + } + } + override suspend fun hideProduct(id: Int) { db.withTransaction { productDao.getProduct(id)?.let { product -> diff --git a/componentCounter/src/main/java/cz/damat/thebeercounter/componentCounter/domain/repository/ProductRepository.kt b/componentCounter/src/main/java/cz/damat/thebeercounter/componentCounter/domain/repository/ProductRepository.kt index dfca774..4139204 100644 --- a/componentCounter/src/main/java/cz/damat/thebeercounter/componentCounter/domain/repository/ProductRepository.kt +++ b/componentCounter/src/main/java/cz/damat/thebeercounter/componentCounter/domain/repository/ProductRepository.kt @@ -12,6 +12,8 @@ import org.koin.core.component.KoinComponent interface ProductRepository : KoinComponent { + suspend fun getProduct(id: Int): Product? + fun getShownProductsFlow(): Flow> //todo - use for allowing the user to delete currently not shown products @@ -23,6 +25,8 @@ interface ProductRepository : KoinComponent { suspend fun setProductCount(id: Int, count: Int, type: HistoryItemType) + suspend fun updateProductNameAndSetCount(id: Int, name: String, count: Int) + suspend fun hideProduct(id: Int) suspend fun clearAllAndAddInitialProduct(initialItemName: String) diff --git a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/Koin.kt b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/Koin.kt index f17aa1a..7ab9b49 100644 --- a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/Koin.kt +++ b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/Koin.kt @@ -1,6 +1,7 @@ package cz.damat.thebeercounter.featureCounter import cz.damat.thebeercounter.featureCounter.scene.counter.CounterScreenViewModel +import cz.damat.thebeercounter.featureCounter.scene.edit.EditScreenViewModel import cz.damat.thebeercounter.featureCounter.scene.history.HistoryViewModel import org.koin.android.ext.koin.androidApplication import org.koin.androidx.viewmodel.dsl.viewModel @@ -14,6 +15,11 @@ val featureCounterKoinModule = module { viewModel { CounterScreenViewModel(get(), androidApplication().resources) } + + viewModel { (productId: Int) -> + EditScreenViewModel(productId, get()) + } + viewModel { HistoryViewModel(get()) } diff --git a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterCommand.kt b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterCommand.kt index c6bf411..0d7bbe5 100644 --- a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterCommand.kt +++ b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterCommand.kt @@ -13,4 +13,5 @@ sealed class CounterCommand : ViewCommand { object ShowAddNewDialog : CounterCommand() object PerformHapticFeedback : CounterCommand() data class ShowSetCountDialog(val product: Product) : CounterCommand() + data class OpenEdit(val productId: Int) : CounterCommand() } diff --git a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterScreen.kt b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterScreen.kt index afa6f7a..30e6a41 100644 --- a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterScreen.kt +++ b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterScreen.kt @@ -18,10 +18,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.navigation.NavController import cz.damat.thebeercounter.commonUI.R -import cz.damat.thebeercounter.commonUI.compose.component.CardThemed +import cz.damat.thebeercounter.commonUI.compose.component.TBCCard import cz.damat.thebeercounter.commonUI.compose.component.ConfirmDialog import cz.damat.thebeercounter.commonUI.compose.component.DropdownItem +import cz.damat.thebeercounter.commonUI.compose.component.TBCDropdownMenu import cz.damat.thebeercounter.commonUI.utils.Previews import cz.damat.thebeercounter.commonUI.utils.collectCommand import cz.damat.thebeercounter.commonUI.utils.collectStateWithLifecycle @@ -31,6 +33,7 @@ import cz.damat.thebeercounter.featureCounter.scene.counter.dialog.AddNewProduct import cz.damat.thebeercounter.featureCounter.scene.counter.dialog.SetCountDialog import cz.damat.thebeercounter.commonUI.compose.theme.disabled import cz.damat.thebeercounter.commonUI.compose.utils.vibrateStrong +import cz.damat.thebeercounter.featureCounter.scene.dashboard.RouteEdit import org.koin.androidx.compose.get import java.text.NumberFormat import java.util.* @@ -40,11 +43,18 @@ import java.util.* * Created by MD on 23.04.23. */ @Composable -fun CounterScreen() { +fun CounterScreen( + navController: NavController +) { val viewModel: CounterScreenViewModel = get() val viewState = viewModel.collectStateWithLifecycle() val onEvent = viewModel.getOnEvent() - CommandCollector(viewModel = viewModel, onEvent) + + CommandCollector( + viewModel, + navController, + onEvent + ) CounterScreenContent(viewState = viewState.value, onEvent = onEvent) } @@ -58,6 +68,7 @@ private fun Preview() { @Composable private fun CommandCollector( viewModel: CounterScreenViewModel, + navController: NavController, onEvent: OnEvent ) { val showSetCountDialogForProduct = remember { @@ -84,6 +95,9 @@ private fun CommandCollector( CounterCommand.PerformHapticFeedback -> { view.vibrateStrong() } + is CounterCommand.OpenEdit -> { + navController.navigate("$RouteEdit/${it.productId}") + } } } @@ -181,7 +195,7 @@ private fun CounterItem( mutableStateOf(false) } - CardThemed( + TBCCard( modifier = Modifier .fillMaxWidth(), borderColor = Color.White, @@ -258,14 +272,14 @@ private fun ProductDropdown(shown: MutableState, product: Product, onEv } ) } - cz.damat.thebeercounter.commonUI.compose.component.DropdownMenu(dropdownShown = shown, scaffoldState = null, items = dropdownItems) + TBCDropdownMenu(dropdownShown = shown, scaffoldState = null, items = dropdownItems) } enum class MenuItem(@StringRes val titleRes: Int) { + Edit(R.string.action_edit), Reset(R.string.action_reset), Hide(R.string.action_delete), SetCount(R.string.action_set_count), - //todo - modify product item and dialog } @Composable diff --git a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterViewModel.kt b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterViewModel.kt index 3a3538a..edc42dd 100644 --- a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterViewModel.kt +++ b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterViewModel.kt @@ -61,6 +61,9 @@ class CounterScreenViewModel( private fun onMenuItemClick(menuItem: MenuItem, id: Int) { ioScope.launch { when (menuItem) { + MenuItem.Edit -> { + sendCommand(CounterCommand.OpenEdit(id)) + } MenuItem.Reset -> { productRepository.setProductCount(id, 0, HistoryItemType.RESET) } @@ -69,9 +72,7 @@ class CounterScreenViewModel( } MenuItem.SetCount -> { currentState().products?.firstOrNull { it.id == id }?.let { - defaultScope.launch { - sendCommand(CounterCommand.ShowSetCountDialog(it)) - } + sendCommand(CounterCommand.ShowSetCountDialog(it)) } } } @@ -85,9 +86,7 @@ class CounterScreenViewModel( } private fun onClearAllClicked() { - defaultScope.launch { - sendCommand(CounterCommand.ShowClearAllConfirmDialog) - } + sendCommand(CounterCommand.ShowClearAllConfirmDialog) } private fun onClearAllConfirmed() { diff --git a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterViewState.kt b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterViewState.kt index 337ad65..3652fca 100644 --- a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterViewState.kt +++ b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/counter/CounterViewState.kt @@ -1,6 +1,6 @@ package cz.damat.thebeercounter.featureCounter.scene.counter -import cz.damat.thebeercounter.commonUI.base.ViewState +import cz.damat.thebeercounter.commonUI.base.ViewStateDTO import cz.damat.thebeercounter.commonlib.room.entity.Product import kotlinx.collections.immutable.ImmutableList @@ -10,4 +10,4 @@ import kotlinx.collections.immutable.ImmutableList */ data class CounterViewState( val products : ImmutableList? = null -) : ViewState +) : ViewStateDTO diff --git a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/dashboard/DashboardNavigation.kt b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/dashboard/DashboardNavigation.kt index 776d944..1eb65a9 100644 --- a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/dashboard/DashboardNavigation.kt +++ b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/dashboard/DashboardNavigation.kt @@ -11,21 +11,26 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.List import androidx.compose.material.icons.filled.MoreVert import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import cz.damat.thebeercounter.commonUI.R import cz.damat.thebeercounter.featureCounter.scene.counter.CounterScreen import cz.damat.thebeercounter.featureCounter.scene.history.HistoryScreen import cz.damat.thebeercounter.commonUI.compose.theme.medium +import cz.damat.thebeercounter.featureCounter.scene.edit.EditScreen /** * Created by MD on 23.04.23. @@ -40,7 +45,17 @@ fun DashboardNavigation() { NavigationHost(navController, it, DashboardNavigationItem.Counter.route) }, bottomBar = { - BottomBar(navController) + val currentBackStackEntry by navController.currentBackStackEntryAsState() + val hasBottomBar by remember { + derivedStateOf { + val currentRoute = currentBackStackEntry?.destination?.route + DashboardNavigationItem.values().any { it.route == currentRoute } + } + } + + if (hasBottomBar) { + BottomBar(navController) + } } ) } @@ -55,12 +70,25 @@ private fun NavigationHost(navController: NavHostController, paddingValues: Padd DashboardNavigationItem.values().forEach { navigationItem -> composable(navigationItem.route) { when (navigationItem) { - DashboardNavigationItem.Counter -> CounterScreen() + DashboardNavigationItem.Counter -> CounterScreen(navController) DashboardNavigationItem.History -> HistoryScreen() //todo - finish the "more" screen DashboardNavigationItem.More -> Text(text = "Not yet implemented. Some settings will be here, maybe some statistics and other stuff?") } } + + composable( + route = "$RouteEdit/{$RouteArgId}", + arguments = listOf( + navArgument(RouteArgId) { + type = NavType.IntType + }, + ) + ) { entry -> + entry.arguments?.getInt(RouteArgId)?.let { + EditScreen(navController = navController, productId = it) + } + } } } } @@ -114,10 +142,13 @@ private enum class DashboardNavigationItem( @Composable fun getVectorResource(): ImageVector { - return when(this) { - Counter -> ImageVector.vectorResource(id = cz.damat.thebeercounter.commonUI.R.drawable.ic_tally_24) + return when (this) { + Counter -> ImageVector.vectorResource(id = R.drawable.ic_tally_24) History -> Icons.Default.List More -> Icons.Default.MoreVert } } -} \ No newline at end of file +} + +const val RouteEdit = "edit" +const val RouteArgId = "id" \ No newline at end of file diff --git a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditCommand.kt b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditCommand.kt new file mode 100644 index 0000000..396007f --- /dev/null +++ b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditCommand.kt @@ -0,0 +1,12 @@ +package cz.damat.thebeercounter.featureCounter.scene.edit + +import cz.damat.thebeercounter.commonUI.base.ViewCommand + + +/** + * Created by MD on 07.11.23. + */ +sealed class EditCommand : ViewCommand { + + object NavigateBack : EditCommand() +} diff --git a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditEvent.kt b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditEvent.kt new file mode 100644 index 0000000..bf75ccf --- /dev/null +++ b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditEvent.kt @@ -0,0 +1,17 @@ +package cz.damat.thebeercounter.featureCounter.scene.edit + +import cz.damat.thebeercounter.commonUI.base.ViewEvent + + +/** + * Created by MD on 07.11.23. + */ +internal typealias OnEvent = (EditEvent) -> Unit + +sealed class EditEvent : ViewEvent { + + data class OnProductNameChange(val productName: String) : EditEvent() + data class OnProductCountChange(val productCount: String) : EditEvent() + object OnBackClick : EditEvent() + object OnSaveClick : EditEvent() +} diff --git a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditScreen.kt b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditScreen.kt new file mode 100644 index 0000000..ebae56b --- /dev/null +++ b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditScreen.kt @@ -0,0 +1,210 @@ +package cz.damat.thebeercounter.featureCounter.scene.edit + +import android.widget.Space +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeContentPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Delete +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import cz.damat.thebeercounter.commonUI.R +import cz.damat.thebeercounter.commonUI.compose.component.TBCButton +import cz.damat.thebeercounter.commonUI.compose.component.TBCStateWrapper +import cz.damat.thebeercounter.commonUI.compose.component.TBCTextField +import cz.damat.thebeercounter.commonUI.utils.Previews +import cz.damat.thebeercounter.commonUI.utils.collectCommand +import cz.damat.thebeercounter.commonUI.utils.collectStateWithLifecycle +import cz.damat.thebeercounter.commonUI.utils.getOnEvent +import cz.damat.thebeercounter.featureCounter.scene.counter.CounterEvent +import org.koin.androidx.compose.getViewModel +import org.koin.core.parameter.parametersOf + + +/** + * Created by MD on 07.11.23. + */ +@Composable +fun EditScreen( + navController: NavController, + productId: Int, +) { + val viewModel: EditScreenViewModel = getViewModel() { + parametersOf(productId) + } + val viewState = viewModel.collectStateWithLifecycle() + val onEvent = viewModel.getOnEvent() + + CommandCollector(viewModel, navController) + EditScreenContent(viewState = viewState.value, onEvent = onEvent) +} + +@Previews +@Composable +private fun Preview() { + EditScreenContent(viewState = EditViewState(), onEvent = {}) +} + +@Composable +private fun CommandCollector( + viewModel: EditScreenViewModel, + navController: NavController, +) { + viewModel.collectCommand(block = { command -> + when (command) { + EditCommand.NavigateBack -> navController.navigateUp() + } + }) +} + + +@Composable +private fun EditScreenContent( + viewState: EditViewState, + onEvent: OnEvent +) { + Scaffold( + topBar = { + TopAppBar( + backgroundColor = MaterialTheme.colors.background, + title = { + Text(text = stringResource(id = R.string.edit_product)) + }, + navigationIcon = { + IconButton(onClick = { onEvent(EditEvent.OnBackClick) }) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = stringResource(id = R.string.back) + ) + } + }, + ) + } + ) { paddingValues -> + TBCStateWrapper( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), state = viewState.state + ) { + EditForm( + productName = viewState.productName, + productCount = viewState.productCount, + onEvent = onEvent + ) + } + } +} + +@Composable +fun EditForm( + productName: String, + productCount: String, + onEvent: OnEvent +) { + Column( + modifier = Modifier + .padding(16.dp) + .safeContentPadding() + .fillMaxHeight() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + TBCTextField( + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = R.string.name), + value = productName, + onValueChanged = { name -> onEvent(EditEvent.OnProductNameChange(name)) } + ) + + TBCTextField( + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = R.string.count), + value = productCount, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), + trailingIcon = { + Row { + RemoveAddButton(add = false, productCount = productCount, onEvent = onEvent) + RemoveAddButton(add = true, productCount = productCount, onEvent = onEvent) + Spacer(modifier = Modifier.width(8.dp)) + } + }, + onValueChanged = { + onEvent(EditEvent.OnProductCountChange(it)) + } + ) + + Spacer(modifier = Modifier.weight(1f)) + + TBCButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.ok), + enabled = productName.isNotBlank(), + onClick = { onEvent(EditEvent.OnSaveClick) } + ) + } +} + +@Composable +fun RemoveAddButton( + add: Boolean, + productCount: String, + onEvent: OnEvent, +) { + IconButton( + modifier = Modifier.fillMaxHeight(), + onClick = { + productCount.toIntOrNull()?.let { + onEvent(EditEvent.OnProductCountChange((if (add) it + 1 else it - 1).toString())) + } + } + ) { + val imageVector = if (add) { + Icons.Default.Add + } else { + ImageVector.vectorResource(id = R.drawable.ic_remove_24) + } + + val contentDescription = stringResource( + id = if (add) { + R.string.action_add + } else { + R.string.action_remove + } + ) + + Icon( + imageVector = imageVector, + tint = MaterialTheme.colors.onSurface, + contentDescription = contentDescription + ) + } +} diff --git a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditScreenViewModel.kt b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditScreenViewModel.kt new file mode 100644 index 0000000..6db69e5 --- /dev/null +++ b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditScreenViewModel.kt @@ -0,0 +1,61 @@ +package cz.damat.thebeercounter.featureCounter.scene.edit + +import cz.damat.thebeercounter.commonUI.R +import cz.damat.thebeercounter.commonUI.base.BaseViewModel +import cz.damat.thebeercounter.commonUI.base.State +import cz.damat.thebeercounter.componentCounter.domain.repository.ProductRepository +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** + * Created by MD on 07.11.22. + */ +class EditScreenViewModel( + private val productId: Int, + private val productRepository: ProductRepository, +) : BaseViewModel(EditViewState()) { + + init { + ioScope.launch { + val product = productRepository.getProduct(productId) + + if (product == null) { + updateState { copy(state = State.Error(R.string.error_product_not_found)) } + } else { + updateState { + copy( + productName = product.name, + productCount = product.count.toString(), + state = State.Content + ) + } + } + } + } + + override fun onEvent(event: EditEvent) { + when (event) { + is EditEvent.OnProductNameChange -> updateState { copy(productName = event.productName) } + is EditEvent.OnProductCountChange -> updateState { copy(productCount = event.productCount) } + EditEvent.OnBackClick -> sendCommand(EditCommand.NavigateBack) //todo confirm changes discard dialog + EditEvent.OnSaveClick -> save() + } + } + + private fun save() { + currentState().let { + ioScope.launch { + updateState { copy(state = State.Loading) } + + productRepository.updateProductNameAndSetCount( + productId, + it.productName, + it.productCount.toIntOrNull() ?: 0 //todo validations + ) + + updateState { copy(state = State.Content) } + sendCommand(EditCommand.NavigateBack) + } + } + } +} diff --git a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditViewState.kt b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditViewState.kt new file mode 100644 index 0000000..327f61f --- /dev/null +++ b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/edit/EditViewState.kt @@ -0,0 +1,14 @@ +package cz.damat.thebeercounter.featureCounter.scene.edit + +import cz.damat.thebeercounter.commonUI.base.ViewStateDTO +import cz.damat.thebeercounter.commonUI.base.State + + +/** + * Created by MD on 07.11.23. + */ +data class EditViewState( + val state : State = State.Loading, + val productName: String = "", + val productCount: String = "", +) : ViewStateDTO diff --git a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/history/HistoryViewState.kt b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/history/HistoryViewState.kt index 918125b..677ad55 100644 --- a/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/history/HistoryViewState.kt +++ b/featureCounter/src/main/java/cz/damat/thebeercounter/featureCounter/scene/history/HistoryViewState.kt @@ -1,7 +1,7 @@ package cz.damat.thebeercounter.featureCounter.scene.history import androidx.compose.runtime.Immutable -import cz.damat.thebeercounter.commonUI.base.ViewState +import cz.damat.thebeercounter.commonUI.base.ViewStateDTO import cz.damat.thebeercounter.componentCounter.data.dto.HistoryProduct import kotlinx.collections.immutable.ImmutableList import java.time.LocalDate @@ -12,7 +12,7 @@ import java.time.LocalDate */ data class HistoryViewState( val items : ImmutableList? = null, -) : ViewState +) : ViewStateDTO @Immutable data class DayToHistoryDTO(