diff --git a/app/build.gradle b/app/build.gradle index b273448a..d67b20eb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,248 +1,267 @@ -plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' - id 'kotlin-android' - id 'kotlin-kapt' - id 'com.squareup.wire' - id 'dagger.hilt.android.plugin' - id 'com.google.gms.google-services' - id 'com.google.firebase.crashlytics' - id 'com.google.firebase.firebase-perf' - id 'kotlin-parcelize' -} - -apply from: "$rootDir/gradle/test-report.gradle" - -ext.codeCoverage = [ - enabled : true, - fileBlackList: [ - ], - fileWhiteList: [ - // UI - '**/ui/*ViewModel*', - '**/ui/*State*', - // Data - // '**/data/*Environment*', - '**/foundation/datasource/local/*Read*', - '**/foundation/datasource/local/*Write*', - // '**/foundation/datasource/preference/PreferenceManager*', - // Core - '**/foundation/extension/*', - ] -] - -android { - namespace 'com.wisnu.kurniawan.wallee' - compileSdkVersion project.ext.compileSdkVersion - buildToolsVersion project.ext.buildToolsVersion - - defaultConfig { - minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.targetSdkVersion - applicationId project.ext.appId - versionCode project.ext.versionCode - versionName project.ext.versionName - - vectorDrawables { - useSupportLibrary true - } - - javaCompileOptions { - annotationProcessorOptions { - arguments += ["room.schemaLocation": "$projectDir/room-schemas".toString()] - } - } - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - signingConfigs { - release { - Properties properties = new Properties() - properties.load(project.rootProject.file('keystore.properties').newDataInputStream()) - - storeFile file("${properties.getProperty('RELEASE_STORE_FILE')}") - storePassword "${properties.getProperty('RELEASE_STORE_PASSWORD')}" - keyAlias "${properties.getProperty('RELEASE_KEY_ALIAS')}" - keyPassword "${properties.getProperty('RELEASE_KEY_PASSWORD')}" - } - } - - applicationVariants.all { variant -> - def variantName = variant.getName() - def debug = variantName.contains('debug') - def appName - - if (debug) { - appName = app_name_debug - } else { - appName = app_name_release - } - - variant.mergedFlavor.manifestPlaceholders = [ - APP_NAME : appName, - ] - } - - buildTypes { - release { - minifyEnabled true - shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.release - } - benchmark { - initWith buildTypes.release - signingConfig signingConfigs.debug - matchingFallbacks = ['release'] - proguardFiles("benchmark-rules.pro") - } - debug { - applicationIdSuffix ".debug" - } - } - - buildFeatures { - viewBinding true - - // Fix compose compile error - compose true - } - - // Fix compose compile error - composeOptions { - kotlinCompilerExtensionVersion libs.versions.androidxComposeCompiler.get() - } - - compileOptions { - coreLibraryDesugaringEnabled = true - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = "17" - } - - testOptions { - unitTests.returnDefaultValues = true - } -} - -wire { - kotlin { - android = true - } -} - -dependencies { - - /////////////// - // UI SUPPORT - ////// - - implementation libs.androidx.core.ktx - implementation libs.androidx.window - implementation libs.androidx.appcompat - implementation libs.androidx.lifecycle.runtimeCompose - implementation libs.androidx.lifecycle.viewModelCompose - implementation libs.androidx.activity.compose - implementation libs.androidx.navigation.compose - implementation libs.androidx.hilt.navigation.compose - implementation libs.lottie.compose - implementation libs.google.material - implementation libs.wisnu.foundation.core.viewmodel - - implementation libs.google.accompanist.systemuicontroller - - // Startup - implementation libs.androidx.startup - implementation libs.androidx.profileinstaller - - // Compose - // For Compose runtime by default coroutine runtime already included from ui, foundation, implicitly - // Not able to get rid of material lib due to we still use these component and not available yet in material3 - // androidx.compose.material.ModalBottomSheetLayout - implementation libs.androidx.compose.material - implementation libs.androidx.compose.material3 - implementation libs.androidx.compose.material.iconsCore - implementation libs.androidx.compose.material.iconsExtended - implementation libs.androidx.compose.material.navigation - implementation libs.androidx.compose.foundation - implementation libs.androidx.compose.ui - implementation libs.androidx.compose.widget - - - /////////////// - // DATA SUPPORT - ////// - - // SQL - implementation libs.androidx.room.runtime - implementation libs.androidx.room.ktx - kapt libs.androidx.room.compiler - - // Key-value - implementation libs.androidx.dataStore.core - implementation libs.google.protobuf - - // Server - implementation libs.bundles.networking - - - /////////////// - // CORE - ////// - - // Concurrent processing - implementation libs.jetbrains.coroutines.android - - // DI - implementation libs.google.hilt.android - kapt libs.google.hilt.compiler - - // Date time - coreLibraryDesugaring(libs.android.desugarJdkLibs) - implementation libs.wisnu.foundation.core.datetime - - // Logger - implementation libs.wisnu.foundation.core.loggr - implementation libs.wisnu.foundation.lib.lifecycleloggr - - // Analytics - implementation platform(libs.google.firebase) - implementation 'com.google.firebase:firebase-analytics-ktx' - implementation 'com.google.firebase:firebase-perf-ktx' - implementation 'com.google.firebase:firebase-crashlytics-ktx' - implementation libs.wisnu.foundation.core.analytics - implementation libs.wisnu.foundation.lib.analyticsmanager - - /////////////// - // DEBUGGING SUPPORT - ////// - - implementation libs.debugging.compose.uiTooling - - debugImplementation libs.wisnu.foundation.test.debug - releaseImplementation libs.wisnu.foundation.test.debugnoop - benchmarkImplementation libs.wisnu.foundation.test.debugnoop - - - /////////////// - // UNIT TEST SUPPORT - ////// - - testImplementation libs.test.androidx.junit - testImplementation libs.test.robolectric - testImplementation libs.test.junit.old - testImplementation libs.test.junit.oldParams - testImplementation libs.test.coroutines - testImplementation libs.test.turbine - -} - -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { - kotlinOptions { - freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn' - } -} +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'kotlin-android' + id 'com.google.devtools.ksp' + id 'dagger.hilt.android.plugin' + id 'com.google.gms.google-services' + id 'com.google.firebase.crashlytics' + id 'com.google.firebase.firebase-perf' + id 'kotlin-parcelize' + id 'androidx.room' + id 'com.google.protobuf' + id 'org.jetbrains.kotlin.plugin.compose' +} + +apply from: "$rootDir/gradle/test-report.gradle" + +ext.codeCoverage = [ + enabled : true, + fileBlackList: [ + ], + fileWhiteList: [ + // UI + '**/ui/*ViewModel*', + '**/ui/*State*', + // Data + // '**/data/*Environment*', + '**/foundation/datasource/local/*Read*', + '**/foundation/datasource/local/*Write*', + // '**/foundation/datasource/preference/PreferenceManager*', + // Core + '**/foundation/extension/*', + ] +] + +android { + namespace 'com.wisnu.kurniawan.wallee' + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + applicationId project.ext.appId + versionCode project.ext.versionCode + versionName project.ext.versionName + + vectorDrawables { + useSupportLibrary true + } + + room { + schemaDirectory "$projectDir/room-schemas" + } + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + signingConfigs { + release { + Properties properties = new Properties() + properties.load(project.rootProject.file('keystore.properties').newDataInputStream()) + + storeFile file("${properties.getProperty('RELEASE_STORE_FILE')}") + storePassword "${properties.getProperty('RELEASE_STORE_PASSWORD')}" + keyAlias "${properties.getProperty('RELEASE_KEY_ALIAS')}" + keyPassword "${properties.getProperty('RELEASE_KEY_PASSWORD')}" + } + } + + applicationVariants.all { variant -> + def variantName = variant.getName() + def debug = variantName.contains('debug') + def appName + + if (debug) { + appName = app_name_debug + } else { + appName = app_name_release + } + + variant.mergedFlavor.manifestPlaceholders = [ + APP_NAME : appName, + ] + } + + buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } + benchmark { + initWith buildTypes.release + signingConfig signingConfigs.debug + matchingFallbacks = ['release'] + proguardFiles("benchmark-rules.pro") + } + debug { + applicationIdSuffix ".debug" + } + } + + buildFeatures { + viewBinding true + + // Fix compose compile error + compose true + } + + compileOptions { + coreLibraryDesugaringEnabled = true + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + testOptions { + unitTests.returnDefaultValues = true + } +} + +protobuf { + protoc { + artifact = libs.google.protobuf.protoc.get().toString() + } + + // Generates the java Protobuf-lite code for the Protobufs in this project. See + // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation + // for more information. + generateProtoTasks { + all().each { task -> + task.builtins { + java { + option 'lite' + } + } + } + } +} + +androidComponents { + onVariants(selector().all(), { variant -> + afterEvaluate { + def capName = variant.name.capitalize() + tasks.getByName("ksp${capName}Kotlin") { + setSource(tasks.getByName("generate${capName}Proto").outputs) + } + } + }) +} + +dependencies { + + /////////////// + // UI SUPPORT + ////// + + implementation libs.androidx.core.ktx + implementation libs.androidx.window + implementation libs.androidx.appcompat + implementation libs.androidx.lifecycle.runtimeCompose + implementation libs.androidx.lifecycle.viewModelCompose + implementation libs.androidx.activity.compose + implementation libs.androidx.navigation.compose + implementation libs.androidx.hilt.navigation.compose + implementation libs.lottie.compose + implementation libs.google.material + implementation libs.wisnu.foundation.core.viewmodel + + implementation libs.google.accompanist.systemuicontroller + + // Startup + implementation libs.androidx.startup + implementation libs.androidx.profileinstaller + + // Compose + // For Compose runtime by default coroutine runtime already included from ui, foundation, implicitly + // Not able to get rid of material lib due to we still use these component and not available yet in material3 + // androidx.compose.material.ModalBottomSheetLayout + implementation libs.androidx.compose.material + implementation libs.androidx.compose.material3 + implementation libs.androidx.compose.material.iconsCore + implementation libs.androidx.compose.material.iconsExtended + implementation libs.androidx.compose.material.navigation + implementation libs.androidx.compose.foundation + implementation libs.androidx.compose.ui + implementation libs.androidx.compose.widget + + + /////////////// + // DATA SUPPORT + ////// + + // SQL + implementation libs.androidx.room.runtime + implementation libs.androidx.room.ktx + ksp libs.androidx.room.compiler + + // Key-value + implementation libs.androidx.dataStore.core + implementation libs.google.protobuf + + // Server + implementation libs.bundles.networking + + + /////////////// + // CORE + ////// + + // Concurrent processing + implementation libs.jetbrains.coroutines.android + + // DI + implementation libs.google.hilt.android + ksp libs.google.hilt.compiler + + // Date time + coreLibraryDesugaring(libs.android.desugarJdkLibs) + implementation libs.wisnu.foundation.core.datetime + + // Logger + implementation libs.wisnu.foundation.core.loggr + implementation libs.wisnu.foundation.lib.lifecycleloggr + + // Analytics + implementation platform(libs.google.firebase) + implementation 'com.google.firebase:firebase-analytics-ktx' + implementation 'com.google.firebase:firebase-perf-ktx' + implementation 'com.google.firebase:firebase-crashlytics-ktx' + implementation libs.wisnu.foundation.core.analytics + implementation libs.wisnu.foundation.lib.analyticsmanager + + /////////////// + // DEBUGGING SUPPORT + ////// + + implementation libs.debugging.compose.uiTooling + + debugImplementation libs.wisnu.foundation.test.debug + releaseImplementation libs.wisnu.foundation.test.debugnoop + benchmarkImplementation libs.wisnu.foundation.test.debugnoop + + + /////////////// + // UNIT TEST SUPPORT + ////// + + testImplementation libs.test.androidx.junit + testImplementation libs.test.robolectric + testImplementation libs.test.junit.old + testImplementation libs.test.junit.oldParams + testImplementation libs.test.coroutines + testImplementation libs.test.turbine + +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn' + } +} diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/features/account/detail/ui/AccountDetailScreen.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/features/account/detail/ui/AccountDetailScreen.kt index 6554b6e7..f100cd75 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/features/account/detail/ui/AccountDetailScreen.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/features/account/detail/ui/AccountDetailScreen.kt @@ -1,382 +1,396 @@ -package com.wisnu.kurniawan.wallee.features.account.detail.ui - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Check -import androidx.compose.material.icons.rounded.ChevronRight -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.wisnu.kurniawan.wallee.R -import com.wisnu.kurniawan.wallee.foundation.extension.cellShape -import com.wisnu.kurniawan.wallee.foundation.extension.getLabel -import com.wisnu.kurniawan.wallee.foundation.extension.shouldShowDivider -import com.wisnu.kurniawan.wallee.foundation.theme.AlphaDisabled -import com.wisnu.kurniawan.wallee.foundation.theme.MediumRadius -import com.wisnu.kurniawan.wallee.foundation.uicomponent.ActionContentCell -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgBasicTextField -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgContentTitle -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgErrorLabel -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeaderEditMode -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeadlineLabel -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgIcon -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgPageLayout -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgSecondaryButton -import com.wisnu.kurniawan.wallee.foundation.uiextension.paddingCell -import com.wisnu.kurniawan.wallee.foundation.viewmodel.HandleEffect - -@Composable -fun AccountDetailScreen( - viewModel: AccountDetailViewModel, - onClosePage: () -> Unit, - onCancelClick: () -> Unit, - onCategorySectionClick: () -> Unit, -) { - val state by viewModel.state.collectAsStateWithLifecycle() - - val localFocusManager = LocalFocusManager.current - - HandleEffect( - viewModel = viewModel, - ) { - when (it) { - AccountDetailEffect.ClosePage -> { - onClosePage() - } - } - } - - AccountDetailScreen( - state = state, - onSaveClick = { - localFocusManager.clearFocus() - viewModel.dispatch(AccountDetailAction.Save) - }, - onCancelClick = { - localFocusManager.clearFocus() - onCancelClick() - }, - onDeleteClick = { - localFocusManager.clearFocus() - viewModel.dispatch(AccountDetailAction.Delete) - }, - onCategorySectionClick = { - localFocusManager.clearFocus() - onCategorySectionClick() - }, - onTotalAmountChange = { viewModel.dispatch(AccountDetailAction.TotalAmountAction.Change(it)) }, - onNameChange = { viewModel.dispatch(AccountDetailAction.NameChange(it)) }, - onAdjustBalanceReasonClick = { viewModel.dispatch(AccountDetailAction.ClickBalanceReason(it.reason)) } - ) -} - -@Composable -private fun AccountDetailScreen( - state: AccountDetailState, - onSaveClick: () -> Unit, - onCancelClick: () -> Unit, - onDeleteClick: () -> Unit, - onNameChange: (TextFieldValue) -> Unit, - onCategorySectionClick: () -> Unit, - onTotalAmountChange: (TextFieldValue) -> Unit, - onAdjustBalanceReasonClick: (AdjustBalanceReasonItem) -> Unit, -) { - val localFocusManager = LocalFocusManager.current - PgPageLayout( - Modifier.fillMaxSize() - .pointerInput(Unit) { - detectTapGestures( - onTap = { - localFocusManager.clearFocus() - } - ) - } - ) { - PgHeaderEditMode( - isAllowToSave = state.isValid(), - title = state.getTitle(), - onSaveClick = onSaveClick, - onCancelClick = onCancelClick, - ) - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp) - .navigationBarsPadding() - .imePadding() - ) { - item { - if (state.shouldShowDuplicateNameError) { - PgErrorLabel( - text = stringResource(R.string.account_edit_name_duplicate), - modifier = Modifier.padding(start = 16.dp, bottom = 6.dp) - ) - } - - NameSection( - name = state.name, - onNameChange = onNameChange, - isError = state.shouldShowDuplicateNameError - ) - - CategorySection( - categoryName = stringResource(state.selectedAccountType().getLabel()), - onCategorySectionClick = onCategorySectionClick - ) - } - - item { - Spacer(Modifier.height(16.dp)) - } - - item { - AmountSection( - totalAmount = state.totalAmount, - totalAmountDisplay = state.getAmountDisplay(), - onTotalAmountChange = onTotalAmountChange, - ) - } - - AdjustAmountReasonCell( - data = state.adjustBalanceReasonItems, - shouldShow = state.isEditMode, - onItemClick = onAdjustBalanceReasonClick - ) - - if (state.canDelete()) { - item { - Spacer(Modifier.height(32.dp)) - PgSecondaryButton( - modifier = Modifier.fillMaxWidth(), - border = BorderStroke( - width = 1.dp, - color = MaterialTheme.colorScheme.error - ), - onClick = onDeleteClick - ) { - PgContentTitle(text = stringResource(R.string.account_edit_delete), color = MaterialTheme.colorScheme.error) - } - } - } - } - } -} - -@Composable -private fun NameSection( - name: TextFieldValue, - isError: Boolean, - onNameChange: (TextFieldValue) -> Unit, -) { - val titleColor = if (isError) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.onBackground - } - ActionContentCell( - title = stringResource(R.string.account_edit_name), - titleColor = titleColor, - showDivider = true, - insetSize = 70.dp, - shape = RoundedCornerShape( - topStart = MediumRadius, - topEnd = MediumRadius - ), - trailing = { - val focusManager = LocalFocusManager.current - PgBasicTextField( - value = name, - onValueChange = onNameChange, - keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions( - onDone = { - focusManager.clearFocus() - } - ), - placeholderValue = stringResource(R.string.account_edit_name_hint), - singleLine = true - ) - }, - ) -} - -@Composable -private fun CategorySection( - categoryName: String, - onCategorySectionClick: () -> Unit, -) { - ActionContentCell( - title = stringResource(R.string.category), - showDivider = false, - shape = RoundedCornerShape( - bottomStart = MediumRadius, - bottomEnd = MediumRadius - ), - insetSize = 70.dp, - onClick = onCategorySectionClick, - trailing = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - PgContentTitle( - text = categoryName, - color = MaterialTheme.colorScheme.onSurface, - ) - Spacer(Modifier.width(8.dp)) - PgIcon( - imageVector = Icons.Rounded.ChevronRight, - tint = LocalContentColor.current.copy(alpha = AlphaDisabled) - ) - } - }, - ) -} - -@Composable -private fun AmountSection( - totalAmount: TextFieldValue, - totalAmountDisplay: String, - onTotalAmountChange: (TextFieldValue) -> Unit, -) { - val focusRequester = remember { FocusRequester() } - - PgHeadlineLabel( - text = stringResource(R.string.account_edit_balance), - modifier = Modifier.padding(start = 16.dp, bottom = 6.dp) - ) - - Box( - modifier = Modifier.background( - color = MaterialTheme.colorScheme.secondary, - shape = MaterialTheme.shapes.medium - ) - .fillMaxWidth() - .paddingCell() - ) { - PgContentTitle( - text = totalAmountDisplay, - modifier = Modifier - .fillMaxWidth() - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() }, - onClick = { - focusRequester.requestFocus() - } - ) - ) - val localFocusManager = LocalFocusManager.current - PgBasicTextField( - value = totalAmount, - onValueChange = onTotalAmountChange, - modifier = Modifier.focusRequester(focusRequester).alpha(0f).size(1.dp), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal, imeAction = ImeAction.Done), - singleLine = true, - keyboardActions = KeyboardActions( - onDone = { - localFocusManager.clearFocus() - } - ), - ) - } -} - -private inline fun LazyListScope.AdjustAmountReasonCell( - data: List, - shouldShow: Boolean, - noinline onItemClick: (AdjustBalanceReasonItem) -> Unit -) { - if (shouldShow) { - item { - Spacer(Modifier.height(16.dp)) - } - item { - PgHeadlineLabel( - text = stringResource(R.string.balance_account_amount_change_title), - modifier = Modifier.padding(start = 16.dp, bottom = 6.dp) - ) - - val size = data.size - data.forEachIndexed { index, adjustBalanceReasonItem -> - AmountChangeReasonSection( - title = stringResource(adjustBalanceReasonItem.getTitle()), - desc = stringResource(adjustBalanceReasonItem.getDesc()), - showDivider = shouldShowDivider(index, size), - shape = cellShape(index, size), - isSelected = adjustBalanceReasonItem.selected, - onItemClick = { onItemClick(adjustBalanceReasonItem) } - ) - } - } - } -} - -@Composable -private fun AmountChangeReasonSection( - title: String, - desc: String, - showDivider: Boolean, - shape: Shape, - isSelected: Boolean, - onItemClick: () -> Unit, -) { - ActionContentCell( - title = title, - desc = desc, - showDivider = showDivider, - shape = shape, - onClick = onItemClick, - trailing = { - PgIcon( - imageVector = Icons.Rounded.Check, - tint = if (isSelected) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.secondary - }, - ) - - }, - ) -} +package com.wisnu.kurniawan.wallee.features.account.detail.ui + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.ChevronRight +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wisnu.kurniawan.wallee.R +import com.wisnu.kurniawan.wallee.foundation.extension.cellShape +import com.wisnu.kurniawan.wallee.foundation.extension.getLabel +import com.wisnu.kurniawan.wallee.foundation.extension.shouldShowDivider +import com.wisnu.kurniawan.wallee.foundation.theme.AlphaDisabled +import com.wisnu.kurniawan.wallee.foundation.theme.MediumRadius +import com.wisnu.kurniawan.wallee.foundation.uicomponent.ActionContentCell +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgBasicTextField +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgContentTitle +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgErrorLabel +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeaderEditMode +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeadlineLabel +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgIcon +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgPageLayout +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgSecondaryButton +import com.wisnu.kurniawan.wallee.foundation.uiextension.paddingCell +import com.wisnu.kurniawan.wallee.foundation.viewmodel.HandleEffect + +@Composable +fun AccountDetailScreen( + viewModel: AccountDetailViewModel, + onClosePage: () -> Unit, + onCancelClick: () -> Unit, + onCategorySectionClick: () -> Unit, +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + val localFocusManager = LocalFocusManager.current + + HandleEffect( + viewModel = viewModel, + ) { + when (it) { + AccountDetailEffect.ClosePage -> { + onClosePage() + } + } + } + + AccountDetailScreen( + state = state, + onSaveClick = { + localFocusManager.clearFocus() + viewModel.dispatch(AccountDetailAction.Save) + }, + onCancelClick = { + localFocusManager.clearFocus() + onCancelClick() + }, + onDeleteClick = { + localFocusManager.clearFocus() + viewModel.dispatch(AccountDetailAction.Delete) + }, + onCategorySectionClick = { + localFocusManager.clearFocus() + onCategorySectionClick() + }, + onTotalAmountChange = { viewModel.dispatch(AccountDetailAction.TotalAmountAction.Change(it)) }, + onNameChange = { viewModel.dispatch(AccountDetailAction.NameChange(it)) }, + onAdjustBalanceReasonClick = { viewModel.dispatch(AccountDetailAction.ClickBalanceReason(it.reason)) } + ) +} + +@Composable +private fun AccountDetailScreen( + state: AccountDetailState, + onSaveClick: () -> Unit, + onCancelClick: () -> Unit, + onDeleteClick: () -> Unit, + onNameChange: (TextFieldValue) -> Unit, + onCategorySectionClick: () -> Unit, + onTotalAmountChange: (TextFieldValue) -> Unit, + onAdjustBalanceReasonClick: (AdjustBalanceReasonItem) -> Unit, +) { + val localFocusManager = LocalFocusManager.current + PgPageLayout( + Modifier + .fillMaxSize() + .pointerInput(Unit) { + detectTapGestures( + onTap = { + localFocusManager.clearFocus() + } + ) + } + ) { + PgHeaderEditMode( + isAllowToSave = state.isValid(), + title = state.getTitle(), + onSaveClick = onSaveClick, + onCancelClick = onCancelClick, + ) + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + .navigationBarsPadding() + .imePadding() + ) { + item { + if (state.shouldShowDuplicateNameError) { + PgErrorLabel( + text = stringResource(R.string.account_edit_name_duplicate), + modifier = Modifier.padding(start = 16.dp, bottom = 6.dp) + ) + } + + NameSection( + name = state.name, + onNameChange = onNameChange, + isError = state.shouldShowDuplicateNameError + ) + + CategorySection( + categoryName = stringResource(state.selectedAccountType().getLabel()), + onCategorySectionClick = onCategorySectionClick + ) + } + + item { + Spacer(Modifier.height(16.dp)) + } + + item { + AmountSection( + totalAmount = state.totalAmount, + totalAmountDisplay = state.getAmountDisplay(), + onTotalAmountChange = onTotalAmountChange, + ) + } + + AdjustAmountReasonCell( + data = state.adjustBalanceReasonItems, + shouldShow = state.isEditMode, + onItemClick = onAdjustBalanceReasonClick + ) + + if (state.canDelete()) { + item { + Spacer(Modifier.height(32.dp)) + PgSecondaryButton( + modifier = Modifier.fillMaxWidth(), + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.error + ), + onClick = onDeleteClick + ) { + PgContentTitle( + text = stringResource(R.string.account_edit_delete), + color = MaterialTheme.colorScheme.error + ) + } + } + } + } + } +} + +@Composable +private fun NameSection( + name: TextFieldValue, + isError: Boolean, + onNameChange: (TextFieldValue) -> Unit, +) { + val titleColor = if (isError) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onBackground + } + ActionContentCell( + title = stringResource(R.string.account_edit_name), + titleColor = titleColor, + showDivider = true, + insetSize = 70.dp, + shape = RoundedCornerShape( + topStart = MediumRadius, + topEnd = MediumRadius + ), + trailing = { + val focusManager = LocalFocusManager.current + PgBasicTextField( + value = name, + onValueChange = onNameChange, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + } + ), + placeholderValue = stringResource(R.string.account_edit_name_hint), + singleLine = true + ) + }, + ) +} + +@Composable +private fun CategorySection( + categoryName: String, + onCategorySectionClick: () -> Unit, +) { + ActionContentCell( + title = stringResource(R.string.category), + showDivider = false, + shape = RoundedCornerShape( + bottomStart = MediumRadius, + bottomEnd = MediumRadius + ), + insetSize = 70.dp, + onClick = onCategorySectionClick, + trailing = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + PgContentTitle( + text = categoryName, + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.width(8.dp)) + PgIcon( + imageVector = Icons.Rounded.ChevronRight, + tint = LocalContentColor.current.copy(alpha = AlphaDisabled) + ) + } + }, + ) +} + +@Composable +private fun AmountSection( + totalAmount: TextFieldValue, + totalAmountDisplay: String, + onTotalAmountChange: (TextFieldValue) -> Unit, +) { + val focusRequester = remember { FocusRequester() } + + PgHeadlineLabel( + text = stringResource(R.string.account_edit_balance), + modifier = Modifier.padding(start = 16.dp, bottom = 6.dp) + ) + + Box( + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.secondary, + shape = MaterialTheme.shapes.medium + ) + .fillMaxWidth() + .paddingCell() + ) { + PgContentTitle( + text = totalAmountDisplay, + modifier = Modifier + .fillMaxWidth() + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { + focusRequester.requestFocus() + } + ) + ) + val localFocusManager = LocalFocusManager.current + PgBasicTextField( + value = totalAmount, + onValueChange = onTotalAmountChange, + modifier = Modifier + .focusRequester(focusRequester) + .alpha(0f) + .size(1.dp), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Decimal, + imeAction = ImeAction.Done + ), + singleLine = true, + keyboardActions = KeyboardActions( + onDone = { + localFocusManager.clearFocus() + } + ), + ) + } +} + +private inline fun LazyListScope.AdjustAmountReasonCell( + data: List, + shouldShow: Boolean, + noinline onItemClick: (AdjustBalanceReasonItem) -> Unit +) { + if (shouldShow) { + item { + Spacer(Modifier.height(16.dp)) + } + item { + PgHeadlineLabel( + text = stringResource(R.string.balance_account_amount_change_title), + modifier = Modifier.padding(start = 16.dp, bottom = 6.dp) + ) + + val size = data.size + data.forEachIndexed { index, adjustBalanceReasonItem -> + AmountChangeReasonSection( + title = stringResource(adjustBalanceReasonItem.getTitle()), + desc = stringResource(adjustBalanceReasonItem.getDesc()), + showDivider = shouldShowDivider(index, size), + shape = cellShape(index, size), + isSelected = adjustBalanceReasonItem.selected, + onItemClick = { onItemClick(adjustBalanceReasonItem) } + ) + } + } + } +} + +@Composable +private fun AmountChangeReasonSection( + title: String, + desc: String, + showDivider: Boolean, + shape: Shape, + isSelected: Boolean, + onItemClick: () -> Unit, +) { + ActionContentCell( + title = title, + desc = desc, + showDivider = showDivider, + shape = shape, + onClick = onItemClick, + trailing = { + PgIcon( + imageVector = Icons.Rounded.Check, + tint = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.secondary + }, + ) + + }, + ) +} diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/features/dashboard/ui/DashboardHostViewModel.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/features/dashboard/ui/DashboardHostViewModel.kt index afe69b52..7f4f9fb1 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/features/dashboard/ui/DashboardHostViewModel.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/features/dashboard/ui/DashboardHostViewModel.kt @@ -1,49 +1,50 @@ -package com.wisnu.kurniawan.wallee.features.dashboard.ui - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ReceiptLong -import androidx.compose.material.icons.rounded.Wallet -import androidx.lifecycle.viewModelScope -import com.wisnu.foundation.coreviewmodel.StatefulViewModel -import com.wisnu.kurniawan.wallee.R -import com.wisnu.kurniawan.wallee.runtime.navigation.home.BalanceSummaryFlow -import com.wisnu.kurniawan.wallee.runtime.navigation.home.TransactionSummaryFlow -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import kotlinx.coroutines.launch - -@HiltViewModel -class DashboardHostViewModel @Inject constructor() : StatefulViewModel(DashboardHostState(), Unit) { - - init { - initTab() - } - - override fun dispatch(action: Unit) { - - } - - private fun initTab() { - viewModelScope.launch { - setState { copy(sections = initial()) } - } - } - - private fun initial(): List { - return listOf( - DashboardSection( - SectionType.TRANSACTION, - R.string.dashboard_transaction, - Icons.Rounded.ReceiptLong, - TransactionSummaryFlow.Root.route - ), - DashboardSection( - SectionType.BALANCE, - R.string.dashboard_balance, - Icons.Rounded.Wallet, - BalanceSummaryFlow.Root.route - ), - ) - } - -} +package com.wisnu.kurniawan.wallee.features.dashboard.ui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ReceiptLong +import androidx.compose.material.icons.rounded.ReceiptLong +import androidx.compose.material.icons.rounded.Wallet +import androidx.lifecycle.viewModelScope +import com.wisnu.foundation.coreviewmodel.StatefulViewModel +import com.wisnu.kurniawan.wallee.R +import com.wisnu.kurniawan.wallee.runtime.navigation.home.BalanceSummaryFlow +import com.wisnu.kurniawan.wallee.runtime.navigation.home.TransactionSummaryFlow +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch + +@HiltViewModel +class DashboardHostViewModel @Inject constructor() : StatefulViewModel(DashboardHostState(), Unit) { + + init { + initTab() + } + + override fun dispatch(action: Unit) { + + } + + private fun initTab() { + viewModelScope.launch { + setState { copy(sections = initial()) } + } + } + + private fun initial(): List { + return listOf( + DashboardSection( + SectionType.TRANSACTION, + R.string.dashboard_transaction, + Icons.AutoMirrored.Rounded.ReceiptLong, + TransactionSummaryFlow.Root.route + ), + DashboardSection( + SectionType.BALANCE, + R.string.dashboard_balance, + Icons.Rounded.Wallet, + BalanceSummaryFlow.Root.route + ), + ) + } + +} diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/features/onboarding/ui/OnboardingScreen.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/features/onboarding/ui/OnboardingScreen.kt index f84a321d..33ad6ec7 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/features/onboarding/ui/OnboardingScreen.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/features/onboarding/ui/OnboardingScreen.kt @@ -1,304 +1,332 @@ -package com.wisnu.kurniawan.wallee.features.onboarding.ui - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Check -import androidx.compose.material.icons.rounded.Menu -import androidx.compose.material3.Divider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.wisnu.kurniawan.wallee.R -import com.wisnu.kurniawan.wallee.foundation.extension.cellShape -import com.wisnu.kurniawan.wallee.foundation.extension.shouldShowDivider -import com.wisnu.kurniawan.wallee.foundation.theme.AlphaDisabled -import com.wisnu.kurniawan.wallee.foundation.theme.DividerAlpha -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgButton -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeadline1 -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeadline2 -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgIcon -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgIconButton -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgPageLayout -import com.wisnu.kurniawan.wallee.foundation.viewmodel.HandleEffect -import com.wisnu.kurniawan.wallee.model.Currency - -@Composable -fun OnboardingScreen( - viewModel: OnboardingViewModel, - onSettingClick: () -> Unit, - onClosePage: () -> Unit, -) { - val state by viewModel.state.collectAsStateWithLifecycle() - - HandleEffect( - viewModel = viewModel - ) { - when (it) { - OnboardingEffect.ClosePage -> { - onClosePage() - } - } - } - - OnboardingScreen( - state = state, - onItemClick = { - viewModel.dispatch(OnboardingAction.SelectCurrency(it.currency)) - }, - onSaveClick = { - viewModel.dispatch(OnboardingAction.ClickSave) - }, - onSettingClick = onSettingClick - ) -} - -@Composable -private fun OnboardingScreen( - state: OnboardingState, - onSettingClick: () -> Unit, - onItemClick: (CurrencyItem) -> Unit, - onSaveClick: () -> Unit, -) { - PgPageLayout( - modifier = Modifier.fillMaxSize() - ) { - Header( - onSettingClick = onSettingClick, - ) - Box(modifier = Modifier.fillMaxSize().weight(1F)) { - Content( - state = state, - onItemClick = onItemClick, - ) - - Box( - modifier = Modifier - .align(Alignment.BottomStart) - .fillMaxWidth() - .background(color = MaterialTheme.colorScheme.surface), - ) { - PgButton( - modifier = Modifier.fillMaxWidth().padding(16.dp), - onClick = onSaveClick, - enabled = state.canSave(), - ) { - Text( - style = MaterialTheme.typography.titleSmall.copy(color = MaterialTheme.colorScheme.onBackground, fontSize = 16.sp, fontWeight = FontWeight.Medium), - text = stringResource(R.string.transaction_edit_save), color = MaterialTheme.colorScheme.onPrimary - ) - } - } - } - } -} - -@Composable -private fun Header( - onSettingClick: () -> Unit, -) { - Row( - modifier = Modifier - .padding(horizontal = 4.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - PgIconButton(onClick = onSettingClick, color = Color.Transparent) { - PgIcon(imageVector = Icons.Rounded.Menu) - } - } -} - -@Composable -private fun Content( - state: OnboardingState, - onItemClick: (CurrencyItem) -> Unit, -) { - val items by state.rememberGroupedCurrencyItems() - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - item { - PgHeadline1( - text = stringResource(R.string.welcome), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) - } - - item { - SpacerSection() - } - - CurrencyCell( - data = items, - selectedCurrency = state.selectedCurrency, - onItemClick = onItemClick, - ) - - item { - Spacer(modifier = Modifier.height(80.dp)) - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -private inline fun LazyListScope.CurrencyCell( - data: Map>, - selectedCurrency: Currency?, - noinline onItemClick: (CurrencyItem) -> Unit, -) { - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - PgHeadline2(text = stringResource(R.string.onboarding_title)) - } - - SpacerHeadline2() - } - - data.forEach { (title, currencies) -> - if (title.isNotBlank()) { - stickyHeader { - Box( - modifier = Modifier - .fillMaxWidth() - .background(color = MaterialTheme.colorScheme.surface) - ) { - Text( - style = MaterialTheme.typography.titleSmall.copy(color = MaterialTheme.colorScheme.onBackground, fontSize = 16.sp), - text = title, - modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp) - ) - } - } - } - - val size = currencies.size - itemsIndexed( - items = currencies, - key = { index, item -> index.toString() + item.currency.countryCode + item.currency.currencyCode } - ) { index, item -> - CurrencyItemCell( - currencySymbol = item.currencySymbol, - flag = item.flag, - countryName = item.countryName, - selected = item.currency == selectedCurrency, - shape = cellShape(index, size), - showDivider = shouldShowDivider(index, size), - onClick = { onItemClick(item) } - ) - } - } -} - -@Composable -private fun CurrencyItemCell( - currencySymbol: String, - flag: String, - countryName: String, - selected: Boolean, - shape: Shape, - showDivider: Boolean, - onClick: () -> Unit, -) { - Surface( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(shape) - .clickable(onClick = onClick), - shape = shape, - color = MaterialTheme.colorScheme.secondary, - ) { - Column { - Row( - modifier = Modifier.fillMaxWidth().padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Row(modifier = Modifier.weight(.75F)) { - Text( - style = MaterialTheme.typography.titleSmall.copy(color = MaterialTheme.colorScheme.onBackground, fontSize = 16.sp), - text = flag, - modifier = Modifier.padding(end = 8.dp) - ) - - Text( - style = MaterialTheme.typography.titleSmall.copy(color = MaterialTheme.colorScheme.onBackground, fontSize = 16.sp), - text = countryName, - ) - } - - Row( - modifier = Modifier.weight(.25f), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - style = MaterialTheme.typography.headlineMedium - .copy(color = MaterialTheme.colorScheme.onBackground.copy(AlphaDisabled), fontSize = 16.sp), - text = currencySymbol, - modifier = Modifier.padding(end = 8.dp).align(alignment = Alignment.CenterVertically) - ) - - if (selected) { - PgIcon( - imageVector = Icons.Rounded.Check, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.align(alignment = Alignment.CenterVertically).size(20.dp) - ) - } - } - } - - if (showDivider) { - Divider( - modifier = Modifier.padding(start = 16.dp), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = DividerAlpha) - ) - } - } - } -} - -@Composable -private fun SpacerSection() { - Spacer(modifier = Modifier.height(16.dp)) -} - -@Composable -private fun SpacerHeadline2() { - Spacer(Modifier.height(10.dp)) -} +package com.wisnu.kurniawan.wallee.features.onboarding.ui + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Menu +import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wisnu.kurniawan.wallee.R +import com.wisnu.kurniawan.wallee.foundation.extension.cellShape +import com.wisnu.kurniawan.wallee.foundation.extension.shouldShowDivider +import com.wisnu.kurniawan.wallee.foundation.theme.AlphaDisabled +import com.wisnu.kurniawan.wallee.foundation.theme.DividerAlpha +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgButton +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeadline1 +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeadline2 +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgIcon +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgIconButton +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgPageLayout +import com.wisnu.kurniawan.wallee.foundation.viewmodel.HandleEffect +import com.wisnu.kurniawan.wallee.model.Currency + +@Composable +fun OnboardingScreen( + viewModel: OnboardingViewModel, + onSettingClick: () -> Unit, + onClosePage: () -> Unit, +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + HandleEffect( + viewModel = viewModel + ) { + when (it) { + OnboardingEffect.ClosePage -> { + onClosePage() + } + } + } + + OnboardingScreen( + state = state, + onItemClick = { + viewModel.dispatch(OnboardingAction.SelectCurrency(it.currency)) + }, + onSaveClick = { + viewModel.dispatch(OnboardingAction.ClickSave) + }, + onSettingClick = onSettingClick + ) +} + +@Composable +private fun OnboardingScreen( + state: OnboardingState, + onSettingClick: () -> Unit, + onItemClick: (CurrencyItem) -> Unit, + onSaveClick: () -> Unit, +) { + PgPageLayout( + modifier = Modifier.fillMaxSize() + ) { + Header( + onSettingClick = onSettingClick, + ) + Box(modifier = Modifier + .fillMaxSize() + .weight(1F)) { + Content( + state = state, + onItemClick = onItemClick, + ) + + Box( + modifier = Modifier + .align(Alignment.BottomStart) + .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.surface), + ) { + PgButton( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + onClick = onSaveClick, + enabled = state.canSave(), + ) { + Text( + style = MaterialTheme.typography.titleSmall.copy( + color = MaterialTheme.colorScheme.onBackground, + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ), + text = stringResource(R.string.transaction_edit_save), + color = MaterialTheme.colorScheme.onPrimary + ) + } + } + } + } +} + +@Composable +private fun Header( + onSettingClick: () -> Unit, +) { + Row( + modifier = Modifier + .padding(horizontal = 4.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + PgIconButton(onClick = onSettingClick, color = Color.Transparent) { + PgIcon(imageVector = Icons.Rounded.Menu) + } + } +} + +@Composable +private fun Content( + state: OnboardingState, + onItemClick: (CurrencyItem) -> Unit, +) { + val items by state.rememberGroupedCurrencyItems() + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + item { + PgHeadline1( + text = stringResource(R.string.welcome), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + } + + item { + SpacerSection() + } + + CurrencyCell( + data = items, + selectedCurrency = state.selectedCurrency, + onItemClick = onItemClick, + ) + + item { + Spacer(modifier = Modifier.height(80.dp)) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +private inline fun LazyListScope.CurrencyCell( + data: Map>, + selectedCurrency: Currency?, + noinline onItemClick: (CurrencyItem) -> Unit, +) { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + PgHeadline2(text = stringResource(R.string.onboarding_title)) + } + + SpacerHeadline2() + } + + data.forEach { (title, currencies) -> + if (title.isNotBlank()) { + stickyHeader { + Box( + modifier = Modifier + .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.surface) + ) { + Text( + style = MaterialTheme.typography.titleSmall.copy( + color = MaterialTheme.colorScheme.onBackground, + fontSize = 16.sp + ), + text = title, + modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp) + ) + } + } + } + + val size = currencies.size + itemsIndexed( + items = currencies, + key = { index, item -> index.toString() + item.currency.countryCode + item.currency.currencyCode } + ) { index, item -> + CurrencyItemCell( + currencySymbol = item.currencySymbol, + flag = item.flag, + countryName = item.countryName, + selected = item.currency == selectedCurrency, + shape = cellShape(index, size), + showDivider = shouldShowDivider(index, size), + onClick = { onItemClick(item) } + ) + } + } +} + +@Composable +private fun CurrencyItemCell( + currencySymbol: String, + flag: String, + countryName: String, + selected: Boolean, + shape: Shape, + showDivider: Boolean, + onClick: () -> Unit, +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .clip(shape) + .clickable(onClick = onClick), + shape = shape, + color = MaterialTheme.colorScheme.secondary, + ) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row(modifier = Modifier.weight(.75F)) { + Text( + style = MaterialTheme.typography.titleSmall.copy( + color = MaterialTheme.colorScheme.onBackground, + fontSize = 16.sp + ), + text = flag, + modifier = Modifier.padding(end = 8.dp) + ) + + Text( + style = MaterialTheme.typography.titleSmall.copy( + color = MaterialTheme.colorScheme.onBackground, + fontSize = 16.sp + ), + text = countryName, + ) + } + + Row( + modifier = Modifier.weight(.25f), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + style = MaterialTheme.typography.headlineMedium + .copy( + color = MaterialTheme.colorScheme.onBackground.copy(AlphaDisabled), + fontSize = 16.sp + ), + text = currencySymbol, + modifier = Modifier + .padding(end = 8.dp) + .align(alignment = Alignment.CenterVertically) + ) + + if (selected) { + PgIcon( + imageVector = Icons.Rounded.Check, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier + .align(alignment = Alignment.CenterVertically) + .size(20.dp) + ) + } + } + } + + if (showDivider) { + HorizontalDivider( + modifier = Modifier.padding(start = 16.dp), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = DividerAlpha) + ) + } + } + } +} + +@Composable +private fun SpacerSection() { + Spacer(modifier = Modifier.height(16.dp)) +} + +@Composable +private fun SpacerHeadline2() { + Spacer(Modifier.height(10.dp)) +} diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/features/transaction/all/ui/AllTransactionScreen.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/features/transaction/all/ui/AllTransactionScreen.kt index 489c8293..13862b5b 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/features/transaction/all/ui/AllTransactionScreen.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/features/transaction/all/ui/AllTransactionScreen.kt @@ -1,204 +1,211 @@ -package com.wisnu.kurniawan.wallee.features.transaction.all.ui - -import androidx.compose.foundation.clickable -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.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Divider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.wisnu.kurniawan.wallee.R -import com.wisnu.kurniawan.wallee.features.transaction.summary.ui.TransactionItem -import com.wisnu.kurniawan.wallee.features.transaction.summary.ui.getAccountDisplay -import com.wisnu.kurniawan.wallee.features.transaction.summary.ui.getAmountColor -import com.wisnu.kurniawan.wallee.features.transaction.summary.ui.getAmountDisplay -import com.wisnu.kurniawan.wallee.features.transaction.summary.ui.getDateTimeDisplay -import com.wisnu.kurniawan.wallee.features.transaction.summary.ui.getTitle -import com.wisnu.kurniawan.wallee.foundation.extension.getSymbol -import com.wisnu.kurniawan.wallee.foundation.theme.AlphaDisabled -import com.wisnu.kurniawan.wallee.foundation.theme.DividerAlpha -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgAmountLabel -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgContentTitle -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgDateLabel -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgEmpty -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeadline1 -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgPageLayout - -@Composable -fun AllTransactionScreen( - viewModel: AllTransactionViewModel, - onTransactionItemClick: (String) -> Unit, -) { - val state by viewModel.state.collectAsStateWithLifecycle() - val effect by viewModel.effect.collectAsStateWithLifecycle() - - AllTransactionScreen( - state = state, - onTransactionItemClick = { - onTransactionItemClick(it.transactionId) - }, - ) -} - -@Composable -private fun AllTransactionScreen( - state: AllTransactionState, - onTransactionItemClick: (TransactionItem) -> Unit, -) { - PgPageLayout( - modifier = Modifier.fillMaxSize() - ) { - - Content( - state = state, - onTransactionItemClick = onTransactionItemClick, - ) - } -} - -@Composable -private fun Content( - state: AllTransactionState, - onTransactionItemClick: (TransactionItem) -> Unit, -) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - item { - PgHeadline1( - text = stringResource(R.string.all_transaction_headline), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) - } - - item { - SpacerSection() - } - - TransactionCell( - data = state.transactionItems, - onItemClick = onTransactionItemClick, - ) - - item { - Spacer(modifier = Modifier.height(80.dp)) - } - } -} - -private inline fun LazyListScope.TransactionCell( - data: List, - noinline onItemClick: (TransactionItem) -> Unit, -) { - if (data.isEmpty()) { - item { - PgEmpty( - stringResource(R.string.all_transaction_empty), - modifier = Modifier.height(500.dp) - ) - } - } else { - itemsIndexed( - items = data, - key = { _, item -> item.transactionId } - ) { _, item -> - TransactionItemCell( - title = item.getTitle(), - account = item.getAccountDisplay(), - dateTime = item.getDateTimeDisplay(), - amount = item.getAmountDisplay(), - amountSymbol = item.currency.getSymbol(), - amountColor = item.getAmountColor( - MaterialTheme.colorScheme.onBackground - ), - note = item.note, - onClick = { onItemClick(item) } - ) - } - } -} - -@Composable -private fun TransactionItemCell( - title: String, - account: String, - dateTime: String, - amount: String, - amountSymbol: String, - amountColor: Color, - note: String, - onClick: () -> Unit, -) { - Surface( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick), - ) { - Column { - Row( - modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 8.dp, bottom = 2.dp, end = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Bottom, - ) { - PgContentTitle( - text = title, - modifier = Modifier.padding(end = 4.dp) - ) - - PgDateLabel( - text = dateTime, - ) - } - - PgAmountLabel( - amount = amount, - symbol = amountSymbol, - color = amountColor, - modifier = Modifier.fillMaxWidth().padding(start = 16.dp, bottom = 2.dp, end = 16.dp), - ) - - PgContentTitle( - text = account, - color = MaterialTheme.colorScheme.onBackground.copy(AlphaDisabled), - modifier = Modifier.fillMaxWidth().padding(start = 16.dp, bottom = 2.dp, end = 16.dp), - ) - - PgContentTitle( - text = note, - color = MaterialTheme.colorScheme.onBackground.copy(AlphaDisabled), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) - ) - - Divider( - modifier = Modifier.padding(start = 16.dp), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = DividerAlpha) - ) - } - } -} - -@Composable -private fun SpacerSection() { - Spacer(modifier = Modifier.height(16.dp)) -} +package com.wisnu.kurniawan.wallee.features.transaction.all.ui + +import androidx.compose.foundation.clickable +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wisnu.kurniawan.wallee.R +import com.wisnu.kurniawan.wallee.features.transaction.summary.ui.TransactionItem +import com.wisnu.kurniawan.wallee.features.transaction.summary.ui.getAccountDisplay +import com.wisnu.kurniawan.wallee.features.transaction.summary.ui.getAmountColor +import com.wisnu.kurniawan.wallee.features.transaction.summary.ui.getAmountDisplay +import com.wisnu.kurniawan.wallee.features.transaction.summary.ui.getDateTimeDisplay +import com.wisnu.kurniawan.wallee.features.transaction.summary.ui.getTitle +import com.wisnu.kurniawan.wallee.foundation.extension.getSymbol +import com.wisnu.kurniawan.wallee.foundation.theme.AlphaDisabled +import com.wisnu.kurniawan.wallee.foundation.theme.DividerAlpha +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgAmountLabel +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgContentTitle +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgDateLabel +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgEmpty +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeadline1 +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgPageLayout + +@Composable +fun AllTransactionScreen( + viewModel: AllTransactionViewModel, + onTransactionItemClick: (String) -> Unit, +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val effect by viewModel.effect.collectAsStateWithLifecycle() + + AllTransactionScreen( + state = state, + onTransactionItemClick = { + onTransactionItemClick(it.transactionId) + }, + ) +} + +@Composable +private fun AllTransactionScreen( + state: AllTransactionState, + onTransactionItemClick: (TransactionItem) -> Unit, +) { + PgPageLayout( + modifier = Modifier.fillMaxSize() + ) { + + Content( + state = state, + onTransactionItemClick = onTransactionItemClick, + ) + } +} + +@Composable +private fun Content( + state: AllTransactionState, + onTransactionItemClick: (TransactionItem) -> Unit, +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + item { + PgHeadline1( + text = stringResource(R.string.all_transaction_headline), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + } + + item { + SpacerSection() + } + + TransactionCell( + data = state.transactionItems, + onItemClick = onTransactionItemClick, + ) + + item { + Spacer(modifier = Modifier.height(80.dp)) + } + } +} + +private inline fun LazyListScope.TransactionCell( + data: List, + noinline onItemClick: (TransactionItem) -> Unit, +) { + if (data.isEmpty()) { + item { + PgEmpty( + stringResource(R.string.all_transaction_empty), + modifier = Modifier.height(500.dp) + ) + } + } else { + itemsIndexed( + items = data, + key = { _, item -> item.transactionId } + ) { _, item -> + TransactionItemCell( + title = item.getTitle(), + account = item.getAccountDisplay(), + dateTime = item.getDateTimeDisplay(), + amount = item.getAmountDisplay(), + amountSymbol = item.currency.getSymbol(), + amountColor = item.getAmountColor( + MaterialTheme.colorScheme.onBackground + ), + note = item.note, + onClick = { onItemClick(item) } + ) + } + } +} + +@Composable +private fun TransactionItemCell( + title: String, + account: String, + dateTime: String, + amount: String, + amountSymbol: String, + amountColor: Color, + note: String, + onClick: () -> Unit, +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + ) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, top = 8.dp, bottom = 2.dp, end = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + ) { + PgContentTitle( + text = title, + modifier = Modifier.padding(end = 4.dp) + ) + + PgDateLabel( + text = dateTime, + ) + } + + PgAmountLabel( + amount = amount, + symbol = amountSymbol, + color = amountColor, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, bottom = 2.dp, end = 16.dp), + ) + + PgContentTitle( + text = account, + color = MaterialTheme.colorScheme.onBackground.copy(AlphaDisabled), + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, bottom = 2.dp, end = 16.dp), + ) + + PgContentTitle( + text = note, + color = MaterialTheme.colorScheme.onBackground.copy(AlphaDisabled), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + ) + + HorizontalDivider( + modifier = Modifier.padding(start = 16.dp), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = DividerAlpha) + ) + } + } +} + +@Composable +private fun SpacerSection() { + Spacer(modifier = Modifier.height(16.dp)) +} diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/features/transaction/summary/ui/TransactionSummaryScreen.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/features/transaction/summary/ui/TransactionSummaryScreen.kt index 7c499615..6a01e367 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/features/transaction/summary/ui/TransactionSummaryScreen.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/features/transaction/summary/ui/TransactionSummaryScreen.kt @@ -1,585 +1,598 @@ -package com.wisnu.kurniawan.wallee.features.transaction.summary.ui - -import android.os.Bundle -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Add -import androidx.compose.material.icons.rounded.Menu -import androidx.compose.material3.Divider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.wisnu.kurniawan.wallee.R -import com.wisnu.kurniawan.wallee.foundation.extension.cellShape -import com.wisnu.kurniawan.wallee.foundation.extension.getColor -import com.wisnu.kurniawan.wallee.foundation.extension.getSymbol -import com.wisnu.kurniawan.wallee.foundation.extension.shouldShowDivider -import com.wisnu.kurniawan.wallee.foundation.theme.AlphaDisabled -import com.wisnu.kurniawan.wallee.foundation.theme.DividerAlpha -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgAmountLabel -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgContentTitle -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgContentTitle2 -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgDateLabel -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeadline1 -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeadline2 -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeadlineLabel -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgIcon -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgIconButton -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgPageLayout -import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgTextButton -import com.wisnu.kurniawan.wallee.foundation.uicomponent.RoundedLinearProgressIndicator -import com.wisnu.kurniawan.wallee.foundation.uiextension.paddingCell -import com.wisnu.kurniawan.wallee.model.Currency - -@Composable -fun TransactionSummaryScreen( - viewModel: TransactionSummaryViewModel, - route: String?, - arguments: Bundle?, - onSettingClick: () -> Unit, - onClickAddTransaction: () -> Unit, - onLastTransactionItemClick: (String) -> Unit, - onSeeMoreLastTransactionClick: () -> Unit, - onSeeMoreTopExpenseClick: () -> Unit, -) { - val state by viewModel.state.collectAsStateWithLifecycle() - - LaunchedEffect(route) { - viewModel.dispatch(TransactionSummaryAction.NavBackStackEntryChanged(route, arguments)) - } - - TransactionSummaryScreen( - state = state, - onSettingClick = onSettingClick, - onClickAddTransaction = onClickAddTransaction, - onLastTransactionItemClick = { onLastTransactionItemClick(it.transactionId) }, - onSeeMoreLastTransactionClick = onSeeMoreLastTransactionClick, - onSeeMoreTopExpenseClick = onSeeMoreTopExpenseClick - ) -} - -@Composable -private fun TransactionSummaryScreen( - state: TransactionSummaryState, - onSettingClick: () -> Unit, - onClickAddTransaction: () -> Unit, - onSeeMoreLastTransactionClick: () -> Unit, - onLastTransactionItemClick: (TransactionItem) -> Unit, - onSeeMoreTopExpenseClick: () -> Unit, -) { - if (!state.isLoading) { - PgPageLayout( - modifier = Modifier.fillMaxSize() - ) { - Header( - onSettingClick = onSettingClick, - onClickAddTransaction = onClickAddTransaction - ) - Content( - state = state, - onSeeMoreLastTransactionClick = onSeeMoreLastTransactionClick, - onLastTransactionItemClick = onLastTransactionItemClick, - onSeeMoreTopExpenseClick = onSeeMoreTopExpenseClick - ) - } - } -} - -@Composable -private fun Header( - onSettingClick: () -> Unit, - onClickAddTransaction: () -> Unit -) { - Row( - modifier = Modifier - .padding(horizontal = 4.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - PgIconButton(onClick = onSettingClick, color = Color.Transparent) { - PgIcon(imageVector = Icons.Rounded.Menu) - } - - PgIconButton(onClick = onClickAddTransaction, color = Color.Transparent) { - PgIcon(imageVector = Icons.Rounded.Add) - } - } -} - -@Composable -private fun Content( - state: TransactionSummaryState, - onSeeMoreLastTransactionClick: () -> Unit, - onLastTransactionItemClick: (TransactionItem) -> Unit, - onSeeMoreTopExpenseClick: () -> Unit, -) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - item { - MainTitleSection( - currentMonth = state.currentMonthDisplay() - ) - } - - item { - SpacerSection() - } - - item { - CashFlowSection( - cashFlow = state.cashFlow - ) - } - - item { - SpacerSection() - } - - LastTransactionCell( - data = state.transactionItems, - onSeeMoreClick = onSeeMoreLastTransactionClick, - onItemClick = onLastTransactionItemClick, - ) - - item { - SpacerSection() - } - - TopExpenseCell( - data = state.topExpenseItems, - currency = state.cashFlow.currency, - onSeeMoreClick = onSeeMoreTopExpenseClick - ) - - item { - Spacer(modifier = Modifier.height(80.dp)) - } - } -} - -@Composable -private fun MainTitleSection( - currentMonth: String -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) { - PgHeadlineLabel( - text = currentMonth, - modifier = Modifier - ) - - PgHeadline1( - text = stringResource(R.string.transaction_summary), - modifier = Modifier - ) - } -} - -@Composable -private fun CashFlowSection( - cashFlow: CashFlow -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) { - PgHeadline2( - text = stringResource(R.string.transaction_cash_flow) - ) - - SpacerHeadline2() - - Column( - modifier = Modifier.background( - color = MaterialTheme.colorScheme.secondary, - shape = MaterialTheme.shapes.medium - ) - .fillMaxWidth() - .paddingCell() - ) { - PgContentTitle( - text = stringResource(R.string.transaction_this_month), - modifier = Modifier.padding(bottom = 2.dp) - ) - PgAmountLabel( - amount = cashFlow.getTotalAmountDisplay(), - symbol = cashFlow.currency.getSymbol(), - color = cashFlow.getTotalAmountColor( - MaterialTheme.colorScheme.onBackground - ) - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Row( - modifier = Modifier.fillMaxWidth() - ) { - CashFlowContent( - title = stringResource(R.string.transaction_income), - amount = cashFlow.getTotalIncomeDisplay(), - currency = cashFlow.currency, - modifier = Modifier - .weight(1f) - .padding(end = 8.dp), - amountColor = cashFlow.getTotalIncomeColor( - MaterialTheme.colorScheme.onBackground - ) - ) - - Box( - Modifier - .height(42.dp) - .width(1.dp) - .background(color = MaterialTheme.colorScheme.onSurface.copy(alpha = DividerAlpha)) - ) - - CashFlowContent( - title = stringResource(R.string.transaction_expense), - amount = cashFlow.getTotalExpenseDisplay(), - currency = cashFlow.currency, - modifier = Modifier - .weight(1f) - .padding(start = 8.dp), - amountColor = cashFlow.getTotalExpenseColor( - MaterialTheme.colorScheme.onBackground - ) - ) - } - } - } -} - -@Composable -private fun CashFlowContent( - modifier: Modifier, - title: String, - amount: String, - amountColor: Color, - currency: Currency -) { - Column(modifier = modifier) { - PgContentTitle2( - text = title, - modifier = Modifier.padding(bottom = 2.dp) - ) - PgAmountLabel( - amount = amount, - color = amountColor, - symbol = currency.getSymbol() - ) - } -} - -private inline fun LazyListScope.LastTransactionCell( - data: List, - noinline onSeeMoreClick: () -> Unit, - noinline onItemClick: (TransactionItem) -> Unit, -) { - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - PgHeadline2(text = stringResource(R.string.transaction_last)) - PgTextButton( - text = stringResource(R.string.show_more), - modifier = Modifier.align(Alignment.Bottom), - onClick = onSeeMoreClick - ) - } - - SpacerHeadline2() - } - - if (data.isEmpty()) { - item { - Empty( - title = stringResource(R.string.transaction_last_no_data_title), - message = stringResource(R.string.transaction_last_no_data_message) - ) - } - } else { - val size = data.size - itemsIndexed( - items = data, - key = { _, item -> item.transactionId } - ) { index, item -> - TransactionItemCell( - title = item.getTitle(), - account = item.getAccountDisplay(), - dateTime = item.getDateTimeDisplay(), - amount = item.getAmountDisplay(), - amountSymbol = item.currency.getSymbol(), - amountColor = item.getAmountColor( - MaterialTheme.colorScheme.onBackground - ), - note = item.note, - shape = cellShape(index, size), - shouldShowDivider = shouldShowDivider(index, size), - isSelected = item.isSelected, - onClick = { onItemClick(item) }, - textColor = item.getTextColor() - ) - } - } -} - -@Composable -private fun TransactionItemCell( - title: String, - account: String, - dateTime: String, - amount: String, - amountSymbol: String, - note: String, - textColor: Color, - amountColor: Color, - shape: Shape, - shouldShowDivider: Boolean, - isSelected: Boolean, - onClick: () -> Unit, -) { - Surface( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(shape) - .clickable(onClick = onClick), - shape = shape, - color = if (isSelected) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.secondary - } - ) { - Column { - Row( - modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 8.dp, bottom = 2.dp, end = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Bottom, - ) { - PgContentTitle( - text = title, - modifier = Modifier.padding(end = 4.dp), - color = textColor - ) - - PgDateLabel( - text = dateTime, - color = textColor - ) - } - - PgAmountLabel( - amount = amount, - symbol = amountSymbol, - color = amountColor, - modifier = Modifier.fillMaxWidth().padding(start = 16.dp, bottom = 2.dp, end = 16.dp), - ) - - PgContentTitle( - text = account, - color = textColor.copy(AlphaDisabled), - modifier = Modifier.fillMaxWidth().padding(start = 16.dp, bottom = 2.dp, end = 16.dp), - ) - - PgContentTitle( - text = note, - color = textColor.copy(AlphaDisabled), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) - ) - - if (shouldShowDivider) { - PgDivider( - needSpacer = !isSelected, - color = if (isSelected) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = DividerAlpha) - } - ) - } - } - } -} - - -private inline fun LazyListScope.TopExpenseCell( - data: List, - currency: Currency, - noinline onSeeMoreClick: () -> Unit, -) { - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - PgHeadline2(text = stringResource(R.string.transaction_top_expenses)) - PgTextButton( - text = stringResource(R.string.show_more), - modifier = Modifier.align(Alignment.Bottom), - onClick = onSeeMoreClick - ) - } - SpacerHeadline2() - } - - if (data.isEmpty()) { - item { - Empty( - title = stringResource(R.string.transaction_last_no_data_title), - message = stringResource(R.string.transaction_top_expenses_no_data_message) - ) - } - } else { - val size = data.size - itemsIndexed( - items = data, - key = { _, item -> item.categoryType } - ) { index, item -> - TopExpenseItemCell( - title = item.getTitle(), - amount = item.getAmountDisplay(currency), - amountSymbol = currency.getSymbol(), - progress = item.progress, - progressColor = item.categoryType.getColor(), - shape = cellShape(index, size), - ) - } - } -} - -@Composable -private fun TopExpenseItemCell( - title: String, - amount: String, - amountSymbol: String, - progress: Float, - progressColor: Color, - shape: Shape -) { - Surface( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - shape = shape, - color = MaterialTheme.colorScheme.secondary, - ) { - Column( - Modifier.fillMaxWidth() - .paddingCell() - ) { - PgContentTitle( - text = title, - modifier = Modifier.padding(bottom = 2.dp) - ) - - PgAmountLabel( - amount = amount, - symbol = amountSymbol, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp), - ) - - RoundedLinearProgressIndicator( - progress = progress, - modifier = Modifier.fillMaxWidth().height(24.dp), - trackColor = MaterialTheme.colorScheme.onBackground.copy(alpha = AlphaDisabled), - color = progressColor - ) - } - } - -} - -@Composable -private fun Empty( - title: String, - message: String -) { - Box(modifier = Modifier.padding(horizontal = 16.dp)) { - Column( - Modifier - .background( - color = MaterialTheme.colorScheme.secondary, - shape = MaterialTheme.shapes.medium - ) - .fillMaxWidth() - .padding(16.dp) - ) { - PgContentTitle( - text = title, - modifier = Modifier.padding(bottom = 2.dp) - ) - PgContentTitle( - text = message, - color = MaterialTheme.colorScheme.onBackground.copy(AlphaDisabled) - ) - } - } -} - -@Composable -private fun SpacerSection() { - Spacer(modifier = Modifier.height(16.dp)) -} - -@Composable -private fun SpacerHeadline2() { - Spacer(Modifier.height(10.dp)) -} - -@Composable -private fun PgDivider( - needSpacer: Boolean, - color: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = DividerAlpha), -) { - Row { - if (needSpacer) { - Spacer( - Modifier - .width(16.dp) - .height(1.dp) - .background(color = MaterialTheme.colorScheme.secondary) - ) - } - Divider(color = color) - } -} +package com.wisnu.kurniawan.wallee.features.transaction.summary.ui + +import android.os.Bundle +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.Menu +import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wisnu.kurniawan.wallee.R +import com.wisnu.kurniawan.wallee.foundation.extension.cellShape +import com.wisnu.kurniawan.wallee.foundation.extension.getColor +import com.wisnu.kurniawan.wallee.foundation.extension.getSymbol +import com.wisnu.kurniawan.wallee.foundation.extension.shouldShowDivider +import com.wisnu.kurniawan.wallee.foundation.theme.AlphaDisabled +import com.wisnu.kurniawan.wallee.foundation.theme.DividerAlpha +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgAmountLabel +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgContentTitle +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgContentTitle2 +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgDateLabel +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeadline1 +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeadline2 +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgHeadlineLabel +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgIcon +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgIconButton +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgPageLayout +import com.wisnu.kurniawan.wallee.foundation.uicomponent.PgTextButton +import com.wisnu.kurniawan.wallee.foundation.uicomponent.RoundedLinearProgressIndicator +import com.wisnu.kurniawan.wallee.foundation.uiextension.paddingCell +import com.wisnu.kurniawan.wallee.model.Currency + +@Composable +fun TransactionSummaryScreen( + viewModel: TransactionSummaryViewModel, + route: String?, + arguments: Bundle?, + onSettingClick: () -> Unit, + onClickAddTransaction: () -> Unit, + onLastTransactionItemClick: (String) -> Unit, + onSeeMoreLastTransactionClick: () -> Unit, + onSeeMoreTopExpenseClick: () -> Unit, +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + LaunchedEffect(route) { + viewModel.dispatch(TransactionSummaryAction.NavBackStackEntryChanged(route, arguments)) + } + + TransactionSummaryScreen( + state = state, + onSettingClick = onSettingClick, + onClickAddTransaction = onClickAddTransaction, + onLastTransactionItemClick = { onLastTransactionItemClick(it.transactionId) }, + onSeeMoreLastTransactionClick = onSeeMoreLastTransactionClick, + onSeeMoreTopExpenseClick = onSeeMoreTopExpenseClick + ) +} + +@Composable +private fun TransactionSummaryScreen( + state: TransactionSummaryState, + onSettingClick: () -> Unit, + onClickAddTransaction: () -> Unit, + onSeeMoreLastTransactionClick: () -> Unit, + onLastTransactionItemClick: (TransactionItem) -> Unit, + onSeeMoreTopExpenseClick: () -> Unit, +) { + if (!state.isLoading) { + PgPageLayout( + modifier = Modifier.fillMaxSize() + ) { + Header( + onSettingClick = onSettingClick, + onClickAddTransaction = onClickAddTransaction + ) + Content( + state = state, + onSeeMoreLastTransactionClick = onSeeMoreLastTransactionClick, + onLastTransactionItemClick = onLastTransactionItemClick, + onSeeMoreTopExpenseClick = onSeeMoreTopExpenseClick + ) + } + } +} + +@Composable +private fun Header( + onSettingClick: () -> Unit, + onClickAddTransaction: () -> Unit +) { + Row( + modifier = Modifier + .padding(horizontal = 4.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + PgIconButton(onClick = onSettingClick, color = Color.Transparent) { + PgIcon(imageVector = Icons.Rounded.Menu) + } + + PgIconButton(onClick = onClickAddTransaction, color = Color.Transparent) { + PgIcon(imageVector = Icons.Rounded.Add) + } + } +} + +@Composable +private fun Content( + state: TransactionSummaryState, + onSeeMoreLastTransactionClick: () -> Unit, + onLastTransactionItemClick: (TransactionItem) -> Unit, + onSeeMoreTopExpenseClick: () -> Unit, +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + item { + MainTitleSection( + currentMonth = state.currentMonthDisplay() + ) + } + + item { + SpacerSection() + } + + item { + CashFlowSection( + cashFlow = state.cashFlow + ) + } + + item { + SpacerSection() + } + + LastTransactionCell( + data = state.transactionItems, + onSeeMoreClick = onSeeMoreLastTransactionClick, + onItemClick = onLastTransactionItemClick, + ) + + item { + SpacerSection() + } + + TopExpenseCell( + data = state.topExpenseItems, + currency = state.cashFlow.currency, + onSeeMoreClick = onSeeMoreTopExpenseClick + ) + + item { + Spacer(modifier = Modifier.height(80.dp)) + } + } +} + +@Composable +private fun MainTitleSection( + currentMonth: String +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + PgHeadlineLabel( + text = currentMonth, + modifier = Modifier + ) + + PgHeadline1( + text = stringResource(R.string.transaction_summary), + modifier = Modifier + ) + } +} + +@Composable +private fun CashFlowSection( + cashFlow: CashFlow +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + PgHeadline2( + text = stringResource(R.string.transaction_cash_flow) + ) + + SpacerHeadline2() + + Column( + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.secondary, + shape = MaterialTheme.shapes.medium + ) + .fillMaxWidth() + .paddingCell() + ) { + PgContentTitle( + text = stringResource(R.string.transaction_this_month), + modifier = Modifier.padding(bottom = 2.dp) + ) + PgAmountLabel( + amount = cashFlow.getTotalAmountDisplay(), + symbol = cashFlow.currency.getSymbol(), + color = cashFlow.getTotalAmountColor( + MaterialTheme.colorScheme.onBackground + ) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth() + ) { + CashFlowContent( + title = stringResource(R.string.transaction_income), + amount = cashFlow.getTotalIncomeDisplay(), + currency = cashFlow.currency, + modifier = Modifier + .weight(1f) + .padding(end = 8.dp), + amountColor = cashFlow.getTotalIncomeColor( + MaterialTheme.colorScheme.onBackground + ) + ) + + Box( + Modifier + .height(42.dp) + .width(1.dp) + .background(color = MaterialTheme.colorScheme.onSurface.copy(alpha = DividerAlpha)) + ) + + CashFlowContent( + title = stringResource(R.string.transaction_expense), + amount = cashFlow.getTotalExpenseDisplay(), + currency = cashFlow.currency, + modifier = Modifier + .weight(1f) + .padding(start = 8.dp), + amountColor = cashFlow.getTotalExpenseColor( + MaterialTheme.colorScheme.onBackground + ) + ) + } + } + } +} + +@Composable +private fun CashFlowContent( + modifier: Modifier, + title: String, + amount: String, + amountColor: Color, + currency: Currency +) { + Column(modifier = modifier) { + PgContentTitle2( + text = title, + modifier = Modifier.padding(bottom = 2.dp) + ) + PgAmountLabel( + amount = amount, + color = amountColor, + symbol = currency.getSymbol() + ) + } +} + +private inline fun LazyListScope.LastTransactionCell( + data: List, + noinline onSeeMoreClick: () -> Unit, + noinline onItemClick: (TransactionItem) -> Unit, +) { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + PgHeadline2(text = stringResource(R.string.transaction_last)) + PgTextButton( + text = stringResource(R.string.show_more), + modifier = Modifier.align(Alignment.Bottom), + onClick = onSeeMoreClick + ) + } + + SpacerHeadline2() + } + + if (data.isEmpty()) { + item { + Empty( + title = stringResource(R.string.transaction_last_no_data_title), + message = stringResource(R.string.transaction_last_no_data_message) + ) + } + } else { + val size = data.size + itemsIndexed( + items = data, + key = { _, item -> item.transactionId } + ) { index, item -> + TransactionItemCell( + title = item.getTitle(), + account = item.getAccountDisplay(), + dateTime = item.getDateTimeDisplay(), + amount = item.getAmountDisplay(), + amountSymbol = item.currency.getSymbol(), + amountColor = item.getAmountColor( + MaterialTheme.colorScheme.onBackground + ), + note = item.note, + shape = cellShape(index, size), + shouldShowDivider = shouldShowDivider(index, size), + isSelected = item.isSelected, + onClick = { onItemClick(item) }, + textColor = item.getTextColor() + ) + } + } +} + +@Composable +private fun TransactionItemCell( + title: String, + account: String, + dateTime: String, + amount: String, + amountSymbol: String, + note: String, + textColor: Color, + amountColor: Color, + shape: Shape, + shouldShowDivider: Boolean, + isSelected: Boolean, + onClick: () -> Unit, +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .clip(shape) + .clickable(onClick = onClick), + shape = shape, + color = if (isSelected) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.secondary + } + ) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, top = 8.dp, bottom = 2.dp, end = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + ) { + PgContentTitle( + text = title, + modifier = Modifier.padding(end = 4.dp), + color = textColor + ) + + PgDateLabel( + text = dateTime, + color = textColor + ) + } + + PgAmountLabel( + amount = amount, + symbol = amountSymbol, + color = amountColor, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, bottom = 2.dp, end = 16.dp), + ) + + PgContentTitle( + text = account, + color = textColor.copy(AlphaDisabled), + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, bottom = 2.dp, end = 16.dp), + ) + + PgContentTitle( + text = note, + color = textColor.copy(AlphaDisabled), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + ) + + if (shouldShowDivider) { + PgDivider( + needSpacer = !isSelected, + color = if (isSelected) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = DividerAlpha) + } + ) + } + } + } +} + + +private inline fun LazyListScope.TopExpenseCell( + data: List, + currency: Currency, + noinline onSeeMoreClick: () -> Unit, +) { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + PgHeadline2(text = stringResource(R.string.transaction_top_expenses)) + PgTextButton( + text = stringResource(R.string.show_more), + modifier = Modifier.align(Alignment.Bottom), + onClick = onSeeMoreClick + ) + } + SpacerHeadline2() + } + + if (data.isEmpty()) { + item { + Empty( + title = stringResource(R.string.transaction_last_no_data_title), + message = stringResource(R.string.transaction_top_expenses_no_data_message) + ) + } + } else { + val size = data.size + itemsIndexed( + items = data, + key = { _, item -> item.categoryType } + ) { index, item -> + TopExpenseItemCell( + title = item.getTitle(), + amount = item.getAmountDisplay(currency), + amountSymbol = currency.getSymbol(), + progress = item.progress, + progressColor = item.categoryType.getColor(), + shape = cellShape(index, size), + ) + } + } +} + +@Composable +private fun TopExpenseItemCell( + title: String, + amount: String, + amountSymbol: String, + progress: Float, + progressColor: Color, + shape: Shape +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = shape, + color = MaterialTheme.colorScheme.secondary, + ) { + Column( + Modifier + .fillMaxWidth() + .paddingCell() + ) { + PgContentTitle( + text = title, + modifier = Modifier.padding(bottom = 2.dp) + ) + + PgAmountLabel( + amount = amount, + symbol = amountSymbol, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 4.dp), + ) + + RoundedLinearProgressIndicator( + progress = progress, + modifier = Modifier + .fillMaxWidth() + .height(24.dp), + trackColor = MaterialTheme.colorScheme.onBackground.copy(alpha = AlphaDisabled), + color = progressColor + ) + } + } + +} + +@Composable +private fun Empty( + title: String, + message: String +) { + Box(modifier = Modifier.padding(horizontal = 16.dp)) { + Column( + Modifier + .background( + color = MaterialTheme.colorScheme.secondary, + shape = MaterialTheme.shapes.medium + ) + .fillMaxWidth() + .padding(16.dp) + ) { + PgContentTitle( + text = title, + modifier = Modifier.padding(bottom = 2.dp) + ) + PgContentTitle( + text = message, + color = MaterialTheme.colorScheme.onBackground.copy(AlphaDisabled) + ) + } + } +} + +@Composable +private fun SpacerSection() { + Spacer(modifier = Modifier.height(16.dp)) +} + +@Composable +private fun SpacerHeadline2() { + Spacer(Modifier.height(10.dp)) +} + +@Composable +private fun PgDivider( + needSpacer: Boolean, + color: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = DividerAlpha), +) { + Row { + if (needSpacer) { + Spacer( + Modifier + .width(16.dp) + .height(1.dp) + .background(color = MaterialTheme.colorScheme.secondary) + ) + } + HorizontalDivider(color = color) + } +} diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/features/transaction/topexpense/ui/TopExpenseScreen.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/features/transaction/topexpense/ui/TopExpenseScreen.kt index 3a4f3b66..d69d0a5a 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/features/transaction/topexpense/ui/TopExpenseScreen.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/features/transaction/topexpense/ui/TopExpenseScreen.kt @@ -131,7 +131,8 @@ private fun TopExpenseItemCell( .fillMaxWidth(), ) { Column( - Modifier.fillMaxWidth() + Modifier + .fillMaxWidth() .paddingCell() ) { PgContentTitle( @@ -143,12 +144,16 @@ private fun TopExpenseItemCell( amount = amount, symbol = amountSymbol, color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 4.dp), ) RoundedLinearProgressIndicator( progress = progress, - modifier = Modifier.fillMaxWidth().height(24.dp), + modifier = Modifier + .fillMaxWidth() + .height(24.dp), trackColor = MaterialTheme.colorScheme.onBackground.copy(alpha = AlphaDisabled), color = progressColor ) diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/CredentialPreferenceSerializer.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/CredentialPreferenceSerializer.kt index 37692bf6..92c9ca6e 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/CredentialPreferenceSerializer.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/CredentialPreferenceSerializer.kt @@ -9,20 +9,16 @@ import java.io.OutputStream object CredentialPreferenceSerializer : Serializer { - override val defaultValue: CredentialPreference = CredentialPreference(token = "") + override val defaultValue: CredentialPreference = CredentialPreference.getDefaultInstance() - @Suppress("BlockingMethodInNonBlockingContext") override suspend fun readFrom(input: InputStream): CredentialPreference { try { - return CredentialPreference.ADAPTER.decode(input) + return CredentialPreference.parseFrom(input) } catch (exception: IOException) { throw CorruptionException("Cannot read proto", exception) } } - @Suppress("BlockingMethodInNonBlockingContext") - override suspend fun writeTo(t: CredentialPreference, output: OutputStream) { - CredentialPreference.ADAPTER.encode(output, t) - } + override suspend fun writeTo(t: CredentialPreference, output: OutputStream) = t.writeTo(output) } diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/LanguagePreferenceSerializer.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/LanguagePreferenceSerializer.kt index 09720f7c..dbf6115a 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/LanguagePreferenceSerializer.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/LanguagePreferenceSerializer.kt @@ -10,20 +10,16 @@ import java.io.OutputStream object LanguagePreferenceSerializer : Serializer { - override val defaultValue: LanguagePreference = LanguagePreference(Language.ENGLISH.code) + override val defaultValue: LanguagePreference = LanguagePreference.getDefaultInstance() - @Suppress("BlockingMethodInNonBlockingContext") override suspend fun readFrom(input: InputStream): LanguagePreference { try { - return LanguagePreference.ADAPTER.decode(input) + return LanguagePreference.parseFrom(input) } catch (exception: IOException) { throw CorruptionException("Cannot read proto", exception) } } - @Suppress("BlockingMethodInNonBlockingContext") - override suspend fun writeTo(t: LanguagePreference, output: OutputStream) { - LanguagePreference.ADAPTER.encode(output, t) - } + override suspend fun writeTo(t: LanguagePreference, output: OutputStream) = t.writeTo(output) } diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/OnboardingPreferenceSerializer.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/OnboardingPreferenceSerializer.kt index 3d7c9a8d..cfbfc003 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/OnboardingPreferenceSerializer.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/OnboardingPreferenceSerializer.kt @@ -9,20 +9,16 @@ import java.io.OutputStream object OnboardingPreferenceSerializer : Serializer { - override val defaultValue: OnboardingPreference = OnboardingPreference(false) + override val defaultValue: OnboardingPreference = OnboardingPreference.getDefaultInstance() - @Suppress("BlockingMethodInNonBlockingContext") override suspend fun readFrom(input: InputStream): OnboardingPreference { try { - return OnboardingPreference.ADAPTER.decode(input) + return OnboardingPreference.parseFrom(input) } catch (exception: IOException) { throw CorruptionException("Cannot read proto", exception) } } - @Suppress("BlockingMethodInNonBlockingContext") - override suspend fun writeTo(t: OnboardingPreference, output: OutputStream) { - OnboardingPreference.ADAPTER.encode(output, t) - } + override suspend fun writeTo(t: OnboardingPreference, output: OutputStream) = t.writeTo(output) } diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/PreferenceManager.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/PreferenceManager.kt index 287a7943..de27974a 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/PreferenceManager.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/PreferenceManager.kt @@ -1,106 +1,117 @@ -package com.wisnu.kurniawan.wallee.foundation.datasource.preference - -import androidx.datastore.core.DataStore -import com.wisnu.kurniawan.wallee.foundation.datasource.preference.model.CredentialPreference -import com.wisnu.kurniawan.wallee.foundation.datasource.preference.model.LanguagePreference -import com.wisnu.kurniawan.wallee.foundation.datasource.preference.model.OnboardingPreference -import com.wisnu.kurniawan.wallee.foundation.datasource.preference.model.ThemePreference -import com.wisnu.kurniawan.wallee.foundation.datasource.preference.model.UserPreference -import com.wisnu.kurniawan.wallee.foundation.di.DiName -import com.wisnu.kurniawan.wallee.foundation.extension.toLanguage -import com.wisnu.kurniawan.wallee.foundation.extension.toTheme -import com.wisnu.kurniawan.wallee.model.Credential -import com.wisnu.kurniawan.wallee.model.Language -import com.wisnu.kurniawan.wallee.model.Theme -import com.wisnu.kurniawan.wallee.model.User -import javax.inject.Inject -import javax.inject.Named -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext - -class PreferenceManager @Inject constructor( - @Named(DiName.DISPATCHER_IO) private val dispatcher: CoroutineDispatcher, - private val credentialDataStore: DataStore, - private val userDataStore: DataStore, - private val themeDataStore: DataStore, - private val languageDataStore: DataStore, - private val onboardingDataStore: DataStore, -) { - - fun getCredential(): Flow { - return credentialDataStore.data - .map { Credential(it.token) } - .catch { emit(Credential(token = "")) } - .flowOn(dispatcher) - } - - fun getUser(): Flow { - return userDataStore.data - .map { User(it.email) } - .catch { emit(User(email = "")) } - .flowOn(dispatcher) - } - - fun getTheme(): Flow { - return themeDataStore.data.map { it.toTheme() } - .catch { emit(Theme.SYSTEM) } - .flowOn(dispatcher) - } - - fun getLanguage(): Flow { - return languageDataStore.data.map { it.toLanguage() } - .catch { emit(Language.ENGLISH) } - .flowOn(dispatcher) - } - - fun hasFinishOnboarding(): Flow { - return onboardingDataStore.data.map { it.finishOnboarding } - .catch { emit(false) } - .flowOn(dispatcher) - } - - suspend fun setCredential(data: Credential) { - withContext(dispatcher) { - credentialDataStore.updateData { - CredentialPreference(data.token) - } - } - } - - suspend fun setUser(data: User) { - withContext(dispatcher) { - userDataStore.updateData { - UserPreference(data.email) - } - } - } - - suspend fun setTheme(data: Theme) { - withContext(dispatcher) { - themeDataStore.updateData { - ThemePreference(data.value) - } - } - } - - suspend fun setLanguage(data: Language) { - withContext(dispatcher) { - languageDataStore.updateData { - LanguagePreference(data.code) - } - } - } - - suspend fun setFinishOnboarding(finish: Boolean) { - withContext(dispatcher) { - onboardingDataStore.updateData { - OnboardingPreference(finish) - } - } - } - -} +package com.wisnu.kurniawan.wallee.foundation.datasource.preference + +import androidx.datastore.core.DataStore +import com.wisnu.kurniawan.wallee.foundation.datasource.preference.model.CredentialPreference +import com.wisnu.kurniawan.wallee.foundation.datasource.preference.model.LanguagePreference +import com.wisnu.kurniawan.wallee.foundation.datasource.preference.model.OnboardingPreference +import com.wisnu.kurniawan.wallee.foundation.datasource.preference.model.ThemePreference +import com.wisnu.kurniawan.wallee.foundation.datasource.preference.model.UserPreference +import com.wisnu.kurniawan.wallee.foundation.di.DiName +import com.wisnu.kurniawan.wallee.foundation.extension.toLanguage +import com.wisnu.kurniawan.wallee.foundation.extension.toTheme +import com.wisnu.kurniawan.wallee.model.Credential +import com.wisnu.kurniawan.wallee.model.Language +import com.wisnu.kurniawan.wallee.model.Theme +import com.wisnu.kurniawan.wallee.model.User +import javax.inject.Inject +import javax.inject.Named +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +class PreferenceManager @Inject constructor( + @Named(DiName.DISPATCHER_IO) private val dispatcher: CoroutineDispatcher, + private val credentialDataStore: DataStore, + private val userDataStore: DataStore, + private val themeDataStore: DataStore, + private val languageDataStore: DataStore, + private val onboardingDataStore: DataStore, +) { + + fun getCredential(): Flow { + return credentialDataStore.data + .map { Credential(it.token) } + .catch { emit(Credential(token = "")) } + .flowOn(dispatcher) + } + + fun getUser(): Flow { + return userDataStore.data + .map { User(it.email) } + .catch { emit(User(email = "")) } + .flowOn(dispatcher) + } + + fun getTheme(): Flow { + return themeDataStore.data.map { it.toTheme() } + .catch { emit(Theme.SYSTEM) } + .flowOn(dispatcher) + } + + fun getLanguage(): Flow { + return languageDataStore.data.map { it.toLanguage() } + .catch { emit(Language.ENGLISH) } + .flowOn(dispatcher) + } + + fun hasFinishOnboarding(): Flow { + return onboardingDataStore.data.map { it.finishOnboarding } + .catch { emit(false) } + .flowOn(dispatcher) + } + + suspend fun setCredential(data: Credential) { + withContext(dispatcher) { + credentialDataStore.updateData { + CredentialPreference.newBuilder() + .setToken(data.token) + .build() + } + } + } + + suspend fun setUser(data: User) { + withContext(dispatcher) { + userDataStore.updateData { + UserPreference.newBuilder() + .setEmail(data.email) + .build() + } + } + } + + suspend fun setTheme(data: Theme) { + withContext(dispatcher) { + themeDataStore.updateData { + ThemePreference + .newBuilder() + .setValue(data.value) + .build() + } + } + } + + suspend fun setLanguage(data: Language) { + withContext(dispatcher) { + languageDataStore.updateData { + LanguagePreference.newBuilder() + .setCode(data.code) + .build() + } + } + } + + suspend fun setFinishOnboarding(finish: Boolean) { + withContext(dispatcher) { + onboardingDataStore.updateData { + OnboardingPreference.newBuilder() + .setFinishOnboarding(finish) + .build() + } + } + } + +} diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/ThemePreferenceSerializer.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/ThemePreferenceSerializer.kt index 266c353b..f6ad5006 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/ThemePreferenceSerializer.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/ThemePreferenceSerializer.kt @@ -10,20 +10,16 @@ import java.io.OutputStream object ThemePreferenceSerializer : Serializer { - override val defaultValue: ThemePreference = ThemePreference(Theme.SYSTEM.value) + override val defaultValue: ThemePreference = ThemePreference.getDefaultInstance() - @Suppress("BlockingMethodInNonBlockingContext") override suspend fun readFrom(input: InputStream): ThemePreference { try { - return ThemePreference.ADAPTER.decode(input) + return ThemePreference.parseFrom(input) } catch (exception: IOException) { throw CorruptionException("Cannot read proto", exception) } } - @Suppress("BlockingMethodInNonBlockingContext") - override suspend fun writeTo(t: ThemePreference, output: OutputStream) { - ThemePreference.ADAPTER.encode(output, t) - } + override suspend fun writeTo(t: ThemePreference, output: OutputStream) = t.writeTo(output) } diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/UserPreferenceSerializer.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/UserPreferenceSerializer.kt index a28b386e..889b6dff 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/UserPreferenceSerializer.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/datasource/preference/UserPreferenceSerializer.kt @@ -9,20 +9,16 @@ import java.io.OutputStream object UserPreferenceSerializer : Serializer { - override val defaultValue: UserPreference = UserPreference(email = "") + override val defaultValue: UserPreference = UserPreference.getDefaultInstance() - @Suppress("BlockingMethodInNonBlockingContext") override suspend fun readFrom(input: InputStream): UserPreference { try { - return UserPreference.ADAPTER.decode(input) + return UserPreference.parseFrom(input) } catch (exception: IOException) { throw CorruptionException("Cannot read proto", exception) } } - @Suppress("BlockingMethodInNonBlockingContext") - override suspend fun writeTo(t: UserPreference, output: OutputStream) { - UserPreference.ADAPTER.encode(output, t) - } + override suspend fun writeTo(t: UserPreference, output: OutputStream) = t.writeTo(output) } diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/extension/ThemeExt.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/extension/ThemeExt.kt index cd85262b..d9434877 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/extension/ThemeExt.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/extension/ThemeExt.kt @@ -1,14 +1,14 @@ -package com.wisnu.kurniawan.wallee.foundation.extension - -import com.wisnu.kurniawan.wallee.foundation.datasource.preference.model.ThemePreference -import com.wisnu.kurniawan.wallee.model.Theme - -fun ThemePreference.toTheme() = when (this.value_) { - Theme.LIGHT.value -> Theme.LIGHT - Theme.TWILIGHT.value -> Theme.TWILIGHT - Theme.NIGHT.value -> Theme.NIGHT - Theme.SUNRISE.value -> Theme.SUNRISE - Theme.AURORA.value -> Theme.AURORA - Theme.WALLPAPER.value -> Theme.WALLPAPER - else -> Theme.SYSTEM -} +package com.wisnu.kurniawan.wallee.foundation.extension + +import com.wisnu.kurniawan.wallee.foundation.datasource.preference.model.ThemePreference +import com.wisnu.kurniawan.wallee.model.Theme + +fun ThemePreference.toTheme() = when (this.value) { + Theme.LIGHT.value -> Theme.LIGHT + Theme.TWILIGHT.value -> Theme.TWILIGHT + Theme.NIGHT.value -> Theme.NIGHT + Theme.SUNRISE.value -> Theme.SUNRISE + Theme.AURORA.value -> Theme.AURORA + Theme.WALLPAPER.value -> Theme.WALLPAPER + else -> Theme.SYSTEM +} diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/theme/Theme.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/theme/Theme.kt index c3f80a70..65bd8f50 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/theme/Theme.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/theme/Theme.kt @@ -1,168 +1,170 @@ -package com.wisnu.kurniawan.wallee.foundation.theme - -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.wisnu.kurniawan.wallee.R -import com.wisnu.kurniawan.wallee.model.Theme - -val LightColorPalette = lightColorScheme( - primary = LightPrimary, - primaryContainer = LightPrimary, - secondary = LightItemBackgroundL1, - secondaryContainer = LightItemBackgroundL1, - background = LightBackgroundL2, - surface = LightBackgroundL1, - surfaceVariant = LightItemBackgroundL2, - error = LightError, - onPrimary = Color.White, - onPrimaryContainer = Color.White, - onSecondary = LightOn, - onSecondaryContainer = LightOn, - onBackground = LightOn, - onSurface = LightOn, - onSurfaceVariant = LightOn, - onError = Color.White -) - -val TwilightColorPalette = lightColorScheme( - primary = TwilightPrimary, - primaryContainer = TwilightPrimary, - secondary = TwilightItemBackgroundL1, - secondaryContainer = TwilightItemBackgroundL1, - background = TwilightBackgroundL2, - surface = TwilightBackgroundL1, - surfaceVariant = TwilightItemBackgroundL2, - error = Error, - onPrimary = TwilightOn, - onPrimaryContainer = TwilightOn, - onSecondary = TwilightOn, - onSecondaryContainer = TwilightOn, - onBackground = TwilightOn, - onSurface = TwilightOn, - onSurfaceVariant = TwilightOn, - onError = Color.White -) - -val NightColorPalette = darkColorScheme( - primary = NightPrimary, - primaryContainer = NightPrimary, - secondary = NightItemBackgroundL1, - secondaryContainer = NightItemBackgroundL1, - background = NightBackgroundL2, - surface = NightBackgroundL1, - surfaceVariant = NightItemBackgroundL2, - error = Error, - onPrimary = NightOn, - onPrimaryContainer = NightOn, - onSecondary = NightOn, - onSecondaryContainer = NightOn, - onBackground = NightOn, - onSurface = NightOn, - onSurfaceVariant = NightOn, - onError = Color.White -) - -val SunriseColorPalette = darkColorScheme( - primary = SunrisePrimary, - primaryContainer = SunrisePrimary, - secondary = SunriseItemBackgroundL1, - secondaryContainer = SunriseItemBackgroundL1, - background = SunriseBackgroundL2, - surface = SunriseBackgroundL1, - surfaceVariant = SunriseItemBackgroundL2, - error = SunriseError, - onPrimary = SunriseOn, - onPrimaryContainer = SunriseOn, - onSecondary = SunriseOn, - onSecondaryContainer = SunriseOn, - onBackground = SunriseOn, - onSurface = SunriseOn, - onSurfaceVariant = SunriseOn, - onError = Color.White -) - -val AuroraColorPalette = darkColorScheme( - primary = AuroraPrimary, - primaryContainer = AuroraPrimary, - secondary = AuroraItemBackgroundL1, - secondaryContainer = AuroraItemBackgroundL1, - background = AuroraBackgroundL2, - surface = AuroraBackgroundL1, - surfaceVariant = AuroraItemBackgroundL2, - error = Error, - onPrimary = AuroraOn, - onPrimaryContainer = AuroraOn, - onSecondary = AuroraOn, - onSecondaryContainer = AuroraOn, - onBackground = AuroraOn, - onSurface = AuroraOn, - onSurfaceVariant = AuroraOn, - onError = Color.White -) - -@Composable -fun Theme( - theme: Theme, - content: @Composable () -> Unit -) { - val colors = when (theme) { - Theme.SYSTEM -> { - if (isSystemInDarkTheme()) { - NightColorPalette - } else { - LightColorPalette - } - } - Theme.WALLPAPER -> { - if (isSystemInDarkTheme()) { - dynamicDarkColorScheme(LocalContext.current) - } else { - dynamicLightColorScheme(LocalContext.current) - } - } - Theme.LIGHT -> LightColorPalette - Theme.TWILIGHT -> TwilightColorPalette - Theme.NIGHT -> NightColorPalette - Theme.SUNRISE -> SunriseColorPalette - Theme.AURORA -> AuroraColorPalette - } - val darkIcons = colors == LightColorPalette || colors == SunriseColorPalette - val systemUiController = rememberSystemUiController() - val activity = LocalContext.current as AppCompatActivity - - SideEffect { - systemUiController.setSystemBarsColor( - color = Color.Transparent, - darkIcons = darkIcons, - isNavigationBarContrastEnforced = false - ) - } - - LaunchedEffect(colors) { - when (colors) { - LightColorPalette -> activity.setTheme(R.style.Theme_Wallee_Light) - TwilightColorPalette -> activity.setTheme(R.style.Theme_Wallee_Twilight) - NightColorPalette -> activity.setTheme(R.style.Theme_Wallee_Night) - SunriseColorPalette -> activity.setTheme(R.style.Theme_Wallee_Sunrise) - AuroraColorPalette -> activity.setTheme(R.style.Theme_Wallee_Aurora) - } - } - - MaterialTheme( - colorScheme = colors, - typography = Typography, - shapes = Shapes, - content = content - ) -} +package com.wisnu.kurniawan.wallee.foundation.theme + +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.wisnu.kurniawan.wallee.R +import com.wisnu.kurniawan.wallee.model.Theme + +val LightColorPalette = lightColorScheme( + primary = LightPrimary, + primaryContainer = LightPrimary, + secondary = LightItemBackgroundL1, + secondaryContainer = LightItemBackgroundL1, + background = LightBackgroundL2, + surface = LightBackgroundL1, + surfaceVariant = LightItemBackgroundL2, + error = LightError, + onPrimary = Color.White, + onPrimaryContainer = Color.White, + onSecondary = LightOn, + onSecondaryContainer = LightOn, + onBackground = LightOn, + onSurface = LightOn, + onSurfaceVariant = LightOn, + onError = Color.White +) + +val TwilightColorPalette = lightColorScheme( + primary = TwilightPrimary, + primaryContainer = TwilightPrimary, + secondary = TwilightItemBackgroundL1, + secondaryContainer = TwilightItemBackgroundL1, + background = TwilightBackgroundL2, + surface = TwilightBackgroundL1, + surfaceVariant = TwilightItemBackgroundL2, + error = Error, + onPrimary = TwilightOn, + onPrimaryContainer = TwilightOn, + onSecondary = TwilightOn, + onSecondaryContainer = TwilightOn, + onBackground = TwilightOn, + onSurface = TwilightOn, + onSurfaceVariant = TwilightOn, + onError = Color.White +) + +val NightColorPalette = darkColorScheme( + primary = NightPrimary, + primaryContainer = NightPrimary, + secondary = NightItemBackgroundL1, + secondaryContainer = NightItemBackgroundL1, + background = NightBackgroundL2, + surface = NightBackgroundL1, + surfaceVariant = NightItemBackgroundL2, + error = Error, + onPrimary = NightOn, + onPrimaryContainer = NightOn, + onSecondary = NightOn, + onSecondaryContainer = NightOn, + onBackground = NightOn, + onSurface = NightOn, + onSurfaceVariant = NightOn, + onError = Color.White +) + +val SunriseColorPalette = darkColorScheme( + primary = SunrisePrimary, + primaryContainer = SunrisePrimary, + secondary = SunriseItemBackgroundL1, + secondaryContainer = SunriseItemBackgroundL1, + background = SunriseBackgroundL2, + surface = SunriseBackgroundL1, + surfaceVariant = SunriseItemBackgroundL2, + error = SunriseError, + onPrimary = SunriseOn, + onPrimaryContainer = SunriseOn, + onSecondary = SunriseOn, + onSecondaryContainer = SunriseOn, + onBackground = SunriseOn, + onSurface = SunriseOn, + onSurfaceVariant = SunriseOn, + onError = Color.White +) + +val AuroraColorPalette = darkColorScheme( + primary = AuroraPrimary, + primaryContainer = AuroraPrimary, + secondary = AuroraItemBackgroundL1, + secondaryContainer = AuroraItemBackgroundL1, + background = AuroraBackgroundL2, + surface = AuroraBackgroundL1, + surfaceVariant = AuroraItemBackgroundL2, + error = Error, + onPrimary = AuroraOn, + onPrimaryContainer = AuroraOn, + onSecondary = AuroraOn, + onSecondaryContainer = AuroraOn, + onBackground = AuroraOn, + onSurface = AuroraOn, + onSurfaceVariant = AuroraOn, + onError = Color.White +) + +@Composable +fun Theme( + theme: Theme, + content: @Composable () -> Unit +) { + val colors = when (theme) { + Theme.SYSTEM -> { + if (isSystemInDarkTheme()) { + NightColorPalette + } else { + LightColorPalette + } + } + + Theme.WALLPAPER -> { + if (isSystemInDarkTheme()) { + dynamicDarkColorScheme(LocalContext.current) + } else { + dynamicLightColorScheme(LocalContext.current) + } + } + + Theme.LIGHT -> LightColorPalette + Theme.TWILIGHT -> TwilightColorPalette + Theme.NIGHT -> NightColorPalette + Theme.SUNRISE -> SunriseColorPalette + Theme.AURORA -> AuroraColorPalette + } + val darkIcons = colors == LightColorPalette || colors == SunriseColorPalette + val systemUiController = rememberSystemUiController() + val activity = LocalContext.current as AppCompatActivity + + SideEffect { + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = darkIcons, + isNavigationBarContrastEnforced = false + ) + } + + LaunchedEffect(colors) { + when (colors) { + LightColorPalette -> activity.setTheme(R.style.Theme_Wallee_Light) + TwilightColorPalette -> activity.setTheme(R.style.Theme_Wallee_Twilight) + NightColorPalette -> activity.setTheme(R.style.Theme_Wallee_Night) + SunriseColorPalette -> activity.setTheme(R.style.Theme_Wallee_Sunrise) + AuroraColorPalette -> activity.setTheme(R.style.Theme_Wallee_Aurora) + } + } + + MaterialTheme( + colorScheme = colors, + typography = Typography, + shapes = Shapes, + content = content + ) +} diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/uicomponent/Button.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/uicomponent/Button.kt index 10a3d81f..b67dba69 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/uicomponent/Button.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/uicomponent/Button.kt @@ -1,146 +1,148 @@ -package com.wisnu.kurniawan.wallee.foundation.uicomponent - -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.updateTransition -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ChevronLeft -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -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.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.wisnu.kurniawan.wallee.foundation.theme.AlphaDisabled -import com.wisnu.kurniawan.wallee.foundation.theme.AlphaHigh - -@Composable -fun PgModalBackButton( - onClick: () -> Unit, - imageVector: ImageVector = Icons.Rounded.ChevronLeft -) { - PgIconButton( - onClick = onClick, - modifier = Modifier.size(28.dp) - ) { - PgIcon( - imageVector = imageVector, - ) - } -} - -@Composable -fun PgIconButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - color: Color = MaterialTheme.colorScheme.secondary, - content: @Composable () -> Unit -) { - val shape = CircleShape - IconButton( - onClick = onClick, - modifier = modifier.background( - color = color, - shape = shape - ).clip(shape), - enabled = enabled - ) { - content() - } -} - -@Composable -fun PgButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - content: @Composable RowScope.() -> Unit -) { - Button( - modifier = modifier, - enabled = enabled, - onClick = onClick, - shape = MaterialTheme.shapes.medium, - content = content, - colors = ButtonDefaults.buttonColors( - disabledContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = AlphaDisabled) - ), - ) -} - -@Composable -fun PgSecondaryButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - border: BorderStroke? = ButtonDefaults.outlinedButtonBorder, - content: @Composable RowScope.() -> Unit -) { - OutlinedButton( - modifier = modifier, - onClick = onClick, - border = border, - shape = MaterialTheme.shapes.medium, - content = content - ) -} - -private enum class PressState { Pressed, Released } - -@Composable -fun PgTextButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - text: String, - enabled: Boolean = true, - color: Color = MaterialTheme.colorScheme.primary, - fontWeight: FontWeight = FontWeight.Normal -) { - var currentState: PressState by remember { mutableStateOf(PressState.Released) } - val transition = updateTransition(targetState = currentState, label = "animation") - val alpha: Float by transition.animateFloat(label = "") { state -> - if (state == PressState.Pressed || !enabled) { - AlphaDisabled - } else { - AlphaHigh - } - } - - PgContentTitle( - text = text, - color = color.copy(alpha), - fontWeight = fontWeight, - modifier = modifier.pointerInput(enabled) { - detectTapGestures( - onPress = { - if (enabled) { - currentState = PressState.Pressed - tryAwaitRelease() - currentState = PressState.Released - } - }, - onTap = { - if (enabled) { - onClick() - } - } - ) - } - ) -} +package com.wisnu.kurniawan.wallee.foundation.uicomponent + +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ChevronLeft +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +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.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.wisnu.kurniawan.wallee.foundation.theme.AlphaDisabled +import com.wisnu.kurniawan.wallee.foundation.theme.AlphaHigh + +@Composable +fun PgModalBackButton( + onClick: () -> Unit, + imageVector: ImageVector = Icons.Rounded.ChevronLeft +) { + PgIconButton( + onClick = onClick, + modifier = Modifier.size(28.dp) + ) { + PgIcon( + imageVector = imageVector, + ) + } +} + +@Composable +fun PgIconButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + color: Color = MaterialTheme.colorScheme.secondary, + content: @Composable () -> Unit +) { + val shape = CircleShape + IconButton( + onClick = onClick, + modifier = modifier + .background( + color = color, + shape = shape + ) + .clip(shape), + enabled = enabled + ) { + content() + } +} + +@Composable +fun PgButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + content: @Composable RowScope.() -> Unit +) { + Button( + modifier = modifier, + enabled = enabled, + onClick = onClick, + shape = MaterialTheme.shapes.medium, + content = content, + colors = ButtonDefaults.buttonColors( + disabledContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = AlphaDisabled) + ), + ) +} + +@Composable +fun PgSecondaryButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + border: BorderStroke? = ButtonDefaults.outlinedButtonBorder, + content: @Composable RowScope.() -> Unit +) { + OutlinedButton( + modifier = modifier, + onClick = onClick, + border = border, + shape = MaterialTheme.shapes.medium, + content = content + ) +} + +private enum class PressState { Pressed, Released } + +@Composable +fun PgTextButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + text: String, + enabled: Boolean = true, + color: Color = MaterialTheme.colorScheme.primary, + fontWeight: FontWeight = FontWeight.Normal +) { + var currentState: PressState by remember { mutableStateOf(PressState.Released) } + val transition = updateTransition(targetState = currentState, label = "animation") + val alpha: Float by transition.animateFloat(label = "") { state -> + if (state == PressState.Pressed || !enabled) { + AlphaDisabled + } else { + AlphaHigh + } + } + + PgContentTitle( + text = text, + color = color.copy(alpha), + fontWeight = fontWeight, + modifier = modifier.pointerInput(enabled) { + detectTapGestures( + onPress = { + if (enabled) { + currentState = PressState.Pressed + tryAwaitRelease() + currentState = PressState.Released + } + }, + onTap = { + if (enabled) { + onClick() + } + } + ) + } + ) +} diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/uicomponent/Cell.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/uicomponent/Cell.kt index b27c96b7..2c143caa 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/uicomponent/Cell.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/uicomponent/Cell.kt @@ -1,237 +1,238 @@ -package com.wisnu.kurniawan.wallee.foundation.uicomponent - -import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -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.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Divider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.wisnu.kurniawan.wallee.foundation.theme.AlphaDisabled -import com.wisnu.kurniawan.wallee.foundation.theme.AlphaHigh -import com.wisnu.kurniawan.wallee.foundation.theme.DividerAlpha -import com.wisnu.kurniawan.wallee.foundation.uiextension.paddingCell - -@Composable -fun PgModalCell( - onClick: () -> Unit, - text: String, - color: Color = MaterialTheme.colorScheme.surfaceVariant, - textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, - enabled: Boolean = true, - leftIcon: @Composable (() -> Unit)? = null, - rightIcon: @Composable (() -> Unit)? = null -) { - val colorAlpha = if (enabled) { - AlphaHigh - } else { - AlphaDisabled - } - val onClickState = if (enabled) { - onClick - } else { - {} - } - val indication = if (enabled) { - LocalIndication.current - } else { - null - } - - val shape = MaterialTheme.shapes.medium - Surface( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .height(56.dp) - .clip(shape) - .clickable( - onClick = onClickState, - indication = indication, - interactionSource = remember { MutableInteractionSource() } - ), - shape = shape, - color = color.copy(alpha = colorAlpha), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - if (leftIcon != null) { - Spacer(Modifier.width(8.dp)) - leftIcon() - Spacer(Modifier.width(16.dp)) - } else { - Spacer(Modifier.width(20.dp)) - } - - PgContentTitle( - text = text, - color = textColor.copy(alpha = colorAlpha) - ) - - if (rightIcon != null) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End - ) { - rightIcon() - Spacer(Modifier.size(20.dp)) - } - } - } - } -} - -@Composable -fun ActionContentCell( - title: String, - desc: String = "", - showDivider: Boolean, - shape: Shape, - enabled: Boolean = true, - onClick: (() -> Unit), - trailing: @Composable () -> Unit -) { - val onClickState = if (enabled) { - onClick - } else { - {} - } - val indication = if (enabled) { - LocalIndication.current - } else { - null - } - - Surface( - modifier = Modifier - .fillMaxWidth() - .clip(shape) - .clickable( - onClick = onClickState, - indication = indication, - interactionSource = remember { MutableInteractionSource() } - ), - color = MaterialTheme.colorScheme.secondary, - shape = shape, - ) { - Column { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - .paddingCell() - ) { - Column(modifier = Modifier.weight(1f)) { - PgContentTitle( - text = title - ) - if (desc.isNotBlank()) { - PgContentTitle( - text = desc, - color = MaterialTheme.colorScheme.onBackground.copy(alpha = AlphaDisabled), - ) - } - } - Spacer(Modifier.size(8.dp)) - trailing() - } - - if (showDivider) { - Row { - Spacer( - Modifier - .width(16.dp) - .height(1.dp) - .background(color = MaterialTheme.colorScheme.secondary) - ) - Divider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = DividerAlpha)) - } - } - } - } -} - -@Composable -fun ActionContentCell( - title: String, - titleColor: Color = MaterialTheme.colorScheme.onBackground, - showDivider: Boolean, - shape: Shape, - enabled: Boolean = true, - insetSize: Dp, - onClick: (() -> Unit) = {}, - trailing: @Composable () -> Unit -) { - val onClickState = if (enabled) { - onClick - } else { - {} - } - val indication = if (enabled) { - LocalIndication.current - } else { - null - } - - Surface( - modifier = Modifier - .fillMaxWidth() - .clip(shape) - .clickable( - onClick = onClickState, - indication = indication, - interactionSource = remember { MutableInteractionSource() } - ), - color = MaterialTheme.colorScheme.secondary, - shape = shape, - ) { - Column { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .paddingCell() - ) { - PgContentTitle( - text = title, - modifier = Modifier.width(insetSize), - color = titleColor - ) - Spacer(Modifier.size(8.dp)) - trailing() - } - if (showDivider) { - Row { - Spacer( - Modifier - .width(16.dp) - .height(1.dp) - .background(color = MaterialTheme.colorScheme.secondary) - ) - Divider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = DividerAlpha)) - } - } - } - } -} - +package com.wisnu.kurniawan.wallee.foundation.uicomponent + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.wisnu.kurniawan.wallee.foundation.theme.AlphaDisabled +import com.wisnu.kurniawan.wallee.foundation.theme.AlphaHigh +import com.wisnu.kurniawan.wallee.foundation.theme.DividerAlpha +import com.wisnu.kurniawan.wallee.foundation.uiextension.paddingCell + +@Composable +fun PgModalCell( + onClick: () -> Unit, + text: String, + color: Color = MaterialTheme.colorScheme.surfaceVariant, + textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + enabled: Boolean = true, + leftIcon: @Composable (() -> Unit)? = null, + rightIcon: @Composable (() -> Unit)? = null +) { + val colorAlpha = if (enabled) { + AlphaHigh + } else { + AlphaDisabled + } + val onClickState = if (enabled) { + onClick + } else { + {} + } + val indication = if (enabled) { + LocalIndication.current + } else { + null + } + + val shape = MaterialTheme.shapes.medium + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .height(56.dp) + .clip(shape) + .clickable( + onClick = onClickState, + indication = indication, + interactionSource = remember { MutableInteractionSource() } + ), + shape = shape, + color = color.copy(alpha = colorAlpha), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + if (leftIcon != null) { + Spacer(Modifier.width(8.dp)) + leftIcon() + Spacer(Modifier.width(16.dp)) + } else { + Spacer(Modifier.width(20.dp)) + } + + PgContentTitle( + text = text, + color = textColor.copy(alpha = colorAlpha) + ) + + if (rightIcon != null) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + rightIcon() + Spacer(Modifier.size(20.dp)) + } + } + } + } +} + +@Composable +fun ActionContentCell( + title: String, + desc: String = "", + showDivider: Boolean, + shape: Shape, + enabled: Boolean = true, + onClick: (() -> Unit), + trailing: @Composable () -> Unit +) { + val onClickState = if (enabled) { + onClick + } else { + {} + } + val indication = if (enabled) { + LocalIndication.current + } else { + null + } + + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(shape) + .clickable( + onClick = onClickState, + indication = indication, + interactionSource = remember { MutableInteractionSource() } + ), + color = MaterialTheme.colorScheme.secondary, + shape = shape, + ) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .paddingCell() + ) { + Column(modifier = Modifier.weight(1f)) { + PgContentTitle( + text = title + ) + if (desc.isNotBlank()) { + PgContentTitle( + text = desc, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = AlphaDisabled), + ) + } + } + Spacer(Modifier.size(8.dp)) + trailing() + } + + if (showDivider) { + Row { + Spacer( + Modifier + .width(16.dp) + .height(1.dp) + .background(color = MaterialTheme.colorScheme.secondary) + ) + HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = DividerAlpha)) + } + } + } + } +} + +@Composable +fun ActionContentCell( + title: String, + titleColor: Color = MaterialTheme.colorScheme.onBackground, + showDivider: Boolean, + shape: Shape, + enabled: Boolean = true, + insetSize: Dp, + onClick: (() -> Unit) = {}, + trailing: @Composable () -> Unit +) { + val onClickState = if (enabled) { + onClick + } else { + {} + } + val indication = if (enabled) { + LocalIndication.current + } else { + null + } + + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(shape) + .clickable( + onClick = onClickState, + indication = indication, + interactionSource = remember { MutableInteractionSource() } + ), + color = MaterialTheme.colorScheme.secondary, + shape = shape, + ) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .paddingCell() + ) { + PgContentTitle( + text = title, + modifier = Modifier.width(insetSize), + color = titleColor + ) + Spacer(Modifier.size(8.dp)) + trailing() + } + if (showDivider) { + Row { + Spacer( + Modifier + .width(16.dp) + .height(1.dp) + .background(color = MaterialTheme.colorScheme.secondary) + ) + HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = DividerAlpha)) + } + } + } + } +} + diff --git a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/uicomponent/Tab.kt b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/uicomponent/Tab.kt index e5c75979..13dca0b9 100644 --- a/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/uicomponent/Tab.kt +++ b/app/src/main/java/com/wisnu/kurniawan/wallee/foundation/uicomponent/Tab.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.SecondaryIndicator import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -27,8 +28,9 @@ fun PgTabRow( containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, indicator = { tabPositions -> - TabRowDefaults.Indicator( - modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]) + SecondaryIndicator( + modifier = Modifier + .tabIndicatorOffset(tabPositions[selectedTabIndex]) .padding(horizontal = 8.dp) .clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)), height = 3.dp, diff --git a/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/credential_preference.proto b/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/credential_preference.proto index 29fc307f..73549b1b 100644 --- a/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/credential_preference.proto +++ b/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/credential_preference.proto @@ -1,6 +1,7 @@ syntax = "proto3"; -package com.wisnu.kurniawan.wallee.foundation.datasource.preference.model; +option java_package = "com.wisnu.kurniawan.wallee.foundation.datasource.preference.model"; +option java_multiple_files = true; message CredentialPreference { string token = 1; diff --git a/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/language_preference.proto b/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/language_preference.proto index 9b382fa9..3d0011f6 100644 --- a/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/language_preference.proto +++ b/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/language_preference.proto @@ -1,6 +1,7 @@ syntax = "proto3"; -package com.wisnu.kurniawan.wallee.foundation.datasource.preference.model; +option java_package = "com.wisnu.kurniawan.wallee.foundation.datasource.preference.model"; +option java_multiple_files = true; message LanguagePreference { string code = 1; diff --git a/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/onboarding_preference.proto b/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/onboarding_preference.proto index c15980cf..dea9374b 100644 --- a/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/onboarding_preference.proto +++ b/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/onboarding_preference.proto @@ -1,6 +1,7 @@ syntax = "proto3"; -package com.wisnu.kurniawan.wallee.foundation.datasource.preference.model; +option java_package = "com.wisnu.kurniawan.wallee.foundation.datasource.preference.model"; +option java_multiple_files = true; message OnboardingPreference { bool finishOnboarding = 1; diff --git a/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/theme_preference.proto b/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/theme_preference.proto index 9858807d..15ceed1a 100644 --- a/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/theme_preference.proto +++ b/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/theme_preference.proto @@ -1,7 +1,8 @@ -syntax = "proto3"; - -package com.wisnu.kurniawan.wallee.foundation.datasource.preference.model; - -message ThemePreference { - string value = 1; -} +syntax = "proto3"; + +option java_package = "com.wisnu.kurniawan.wallee.foundation.datasource.preference.model"; +option java_multiple_files = true; + +message ThemePreference { + string value = 1; +} diff --git a/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/user_preference.proto b/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/user_preference.proto index df0bc858..d68fe318 100644 --- a/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/user_preference.proto +++ b/app/src/main/proto/com/wisnu/kurniawan/wallee/foundation/datasource/preference/user_preference.proto @@ -1,6 +1,7 @@ syntax = "proto3"; -package com.wisnu.kurniawan.wallee.foundation.datasource.preference.model; +option java_package = "com.wisnu.kurniawan.wallee.foundation.datasource.preference.model"; +option java_multiple_files = true; message UserPreference { string email = 1; diff --git a/build.gradle b/build.gradle index 83db0331..497c2627 100644 --- a/build.gradle +++ b/build.gradle @@ -1,35 +1,38 @@ -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath libs.android.gradlePlugin - classpath libs.jetbrains.kotlin.gradlePlugin - classpath libs.google.firebase.crashlytics.gradlePlugin - classpath libs.google.firebase.performance.gradlePlugin - classpath libs.google.hilt.gradlePlugin - classpath libs.google.servicesPlugin - classpath libs.squareup.wire.gradlePlugin - } -} - -allprojects { - project.ext { - minSdkVersion = 21 - targetSdkVersion = 34 - compileSdkVersion = 34 - buildToolsVersion = "34.0.0" - appId = project.properties["application_id"] - versionCode = Integer.parseInt(project.properties["version_code"]) - versionName = project.properties["version_name"] - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} - - - +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath libs.android.gradlePlugin + classpath libs.jetbrains.kotlin.gradlePlugin + classpath libs.google.firebase.crashlytics.gradlePlugin + classpath libs.google.firebase.performance.gradlePlugin + classpath libs.google.hilt.gradlePlugin + classpath libs.google.servicesPlugin + classpath libs.google.kspPlugin + classpath libs.room.gradlePlugin + classpath libs.protobuf.gradlePlugin + classpath libs.compose.gradlePlugin + } +} + +allprojects { + project.ext { + minSdkVersion = 21 + targetSdkVersion = 34 + compileSdkVersion = 34 + buildToolsVersion = "34.0.0" + appId = project.properties["application_id"] + versionCode = Integer.parseInt(project.properties["version_code"]) + versionName = project.properties["version_name"] + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a7abf560..ec810e09 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,115 +1,116 @@ -[versions] -androidGradlePlugin = "8.3.2" -protoWirePlugin = "4.9.9" - -accompanist = "0.34.0" -androidDesugarJdkLibs = "2.0.4" -androidxActivity = "1.9.0" -androidxAppCompat = "1.7.0" -androidxCompose = "1.7.0-beta01" -androidxComposeCompiler = "1.5.8" -androidxComposeMaterial3 = "1.3.0-beta04" -androidxCore = "1.13.1" -androidxDataStore = "1.1.1" -androidxHiltNavigationCompose = "1.2.0" -androidxLifecycle = "2.8.3" -androidxMacroBenchmark = "1.2.4" -androidxNavigation = "2.7.7" -androidxProfileinstaller = "1.3.1" -androidxStartup = "1.1.1" -androidxWindow = "1.3.0" -composeLottie = "6.4.1" -composeWidget = "1.1.0" -firebase = "33.1.2" -hilt = "2.51.1" -jUnit = "5.10.2" -kotlin = "1.9.22" -kotlinxCoroutines = "1.8.0" -material = "1.12.0" -okhttp = "4.12.0" -protobuf = "4.26.1" -retrofit = "2.11.0" -room = "2.6.1" -wisnuFoundation = "0.1.3" - - -[libraries] -android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } -google-firebase-crashlytics-gradlePlugin = { group = "com.google.firebase", name = "firebase-crashlytics-gradle", version = "3.0.2" } -google-firebase-performance-gradlePlugin = { group = "com.google.firebase", name = "perf-plugin", version = "1.4.2" } -google-hilt-gradlePlugin = { group = "com.google.dagger", name = "hilt-android-gradle-plugin", version.ref = "hilt" } -google-kspPlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version = "1.9.24-1.0.20" } -google-servicesPlugin = { group = "com.google.gms", name = "google-services", version = "4.4.2" } -jetbrains-kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } -junit-gradlePlugin = { group = "de.mannodermaus.gradle.plugins", name = "android-junit5", version = "1.10.0.0" } -squareup-wire-gradlePlugin = { group = "com.squareup.wire", name = "wire-gradle-plugin", version.ref = "protoWirePlugin" } - -android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" } -androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } -androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } -androidx-benchmark-macro = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "androidxMacroBenchmark" } -androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidxCompose" } -androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "androidxCompose" } -androidx-compose-material-navigation = { group = "androidx.compose.material", name = "material-navigation", version.ref = "androidxCompose" } -androidx-compose-material-iconsCore = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "androidxCompose" } -androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "androidxCompose" } -androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidxComposeMaterial3" } -androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidxCompose" } -androidx-compose-widget = { group = "androidx.glance", name = "glance", version.ref = "composeWidget" } -androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" } -androidx-dataStore-core = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDataStore" } -androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } -androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } -androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } -androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } -androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" } -androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } -androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } -androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } -androidx-startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidxStartup" } -androidx-window = { group = "androidx.window", name = "window", version.ref = "androidxWindow" } -google-accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" } -google-firebase = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase" } -google-hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } -google-hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } -google-material = { group = "com.google.android.material", name = "material", version.ref = "material" } -google-protobuf = { group = "com.google.protobuf", name = "protobuf-javalite", version.ref = "protobuf" } -jetbrains-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } -lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "composeLottie" } -squareup-okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } -squareup-retrofit-converter = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "retrofit" } -squareup-retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } -wisnu-foundation-core-analytics = { group = "com.github.wisnukurniawan.foundation", name = "core-analytics", version.ref = "wisnuFoundation" } -wisnu-foundation-core-datetime = { group = "com.github.wisnukurniawan.foundation", name = "core-date-time", version.ref = "wisnuFoundation" } -wisnu-foundation-core-json = { group = "com.github.wisnukurniawan.foundation", name = "core-json", version.ref = "wisnuFoundation" } -wisnu-foundation-core-loggr = { group = "com.github.wisnukurniawan.foundation", name = "core-loggr", version.ref = "wisnuFoundation" } -wisnu-foundation-core-ui = { group = "com.github.wisnukurniawan.foundation", name = "core-ui", version.ref = "wisnuFoundation" } -wisnu-foundation-core-viewmodel = { group = "com.github.wisnukurniawan.foundation", name = "core-viewmodel", version.ref = "wisnuFoundation" } -wisnu-foundation-lib-analyticsmanager = { group = "com.github.wisnukurniawan.foundation", name = "lib-analytics-manager", version.ref = "wisnuFoundation" } -wisnu-foundation-lib-lifecycleloggr = { group = "com.github.wisnukurniawan.foundation", name = "lib-lifecycle-loggr", version.ref = "wisnuFoundation" } -wisnu-foundation-test-debugnoop = { group = "com.github.wisnukurniawan.foundation", name = "test-debug-no-op", version.ref = "wisnuFoundation" } -wisnu-foundation-test-debug = { group = "com.github.wisnukurniawan.foundation", name = "test-debug", version.ref = "wisnuFoundation" } - -debugging-chucker = { group = "com.github.chuckerteam.chucker", name = "library", version = "4.0.0" } -debugging-compose-uiTooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidxCompose" } -debugging-okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } - -test-androidx-core = { group = "androidx.test", name = "core", version = "1.6.1" } -test-androidx-espressoCore = { group = "androidx.test.espresso", name = "espresso-core", version = "3.6.1" } -test-androidx-ext = { group = "androidx.test.ext", name = "junit-ktx", version = "1.2.1" } -test-androidx-junit = { group = "androidx.test.ext", name = "junit", version = "1.2.1" } -test-androidx-runner = { group = "androidx.test", name = "runner", version = "1.6.1" } -test-androidx-rules = { group = "androidx.test", name = "rules", version = "1.6.1" } -test-androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version = "2.3.0" } -test-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } -test-junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "jUnit" } -test-junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "jUnit" } -test-junit-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "jUnit" } -test-junit-old = { group = "junit", name = "junit", version = "4.13.2" } -test-junit-oldParams = { group = "pl.pragmatists", name = "JUnitParams", version = "1.1.1" } -test-robolectric = { group = "org.robolectric", name = "robolectric", version = "4.12.1" } -test-turbine = { group = "app.cash.turbine", name = "turbine", version = "1.1.0" } - - -[bundles] -networking = ["squareup-retrofit-core", "squareup-retrofit-converter", "squareup-okhttp"] +[versions] +androidGradlePlugin = "8.3.2" + +accompanist = "0.34.0" +androidDesugarJdkLibs = "2.1.3" +androidxActivity = "1.9.3" +androidxAppCompat = "1.7.0" +androidxCompose = "1.7.5" +androidxComposeMaterial3 = "1.3.1" +androidxCore = "1.13.1" +androidxDataStore = "1.1.1" +androidxHiltNavigationCompose = "1.2.0" +androidxLifecycle = "2.8.7" +androidxMacroBenchmark = "1.3.3" +androidxNavigation = "2.8.4" +androidxProfileinstaller = "1.4.1" +androidxStartup = "1.2.0" +androidxWindow = "1.3.0" +composeLottie = "6.4.1" +composeWidget = "1.1.1" +firebase = "33.6.0" +hilt = "2.52" +jUnit = "5.10.2" +kotlin = "2.0.20" +kotlinxCoroutines = "1.9.0" +ksp = "2.0.20-1.0.25" +material = "1.12.0" +okhttp = "4.12.0" +protobuf = "4.26.1" +retrofit = "2.11.0" +room = "2.6.1" +wisnuFoundation = "0.1.3" + + +[libraries] +android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } +compose-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } +google-firebase-crashlytics-gradlePlugin = { group = "com.google.firebase", name = "firebase-crashlytics-gradle", version = "3.0.2" } +google-firebase-performance-gradlePlugin = { group = "com.google.firebase", name = "perf-plugin", version = "1.4.2" } +google-hilt-gradlePlugin = { group = "com.google.dagger", name = "hilt-android-gradle-plugin", version.ref = "hilt" } +google-kspPlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } +google-servicesPlugin = { group = "com.google.gms", name = "google-services", version = "4.4.2" } +jetbrains-kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } +protobuf-gradlePlugin = { group = "com.google.protobuf", name = "protobuf-gradle-plugin", version = "0.9.4" } +room-gradlePlugin = { group = "androidx.room", name = "room-gradle-plugin", version.ref = "room" } + +android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } +androidx-benchmark-macro = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "androidxMacroBenchmark" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidxCompose" } +androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "androidxCompose" } +androidx-compose-material-navigation = { group = "androidx.compose.material", name = "material-navigation", version = "1.7.0-beta01" } +androidx-compose-material-iconsCore = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "androidxCompose" } +androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "androidxCompose" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidxComposeMaterial3" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidxCompose" } +androidx-compose-widget = { group = "androidx.glance", name = "glance", version.ref = "composeWidget" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" } +androidx-dataStore-core = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDataStore" } +androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } +androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } +androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } +androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" } +androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } +androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } +androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +androidx-startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidxStartup" } +androidx-window = { group = "androidx.window", name = "window", version.ref = "androidxWindow" } +google-accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" } +google-firebase = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase" } +google-hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +google-hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } +google-material = { group = "com.google.android.material", name = "material", version.ref = "material" } +google-protobuf = { group = "com.google.protobuf", name = "protobuf-javalite", version.ref = "protobuf" } +google-protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } +jetbrains-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } +lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "composeLottie" } +squareup-okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +squareup-retrofit-converter = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "retrofit" } +squareup-retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +wisnu-foundation-core-analytics = { group = "com.github.wisnukurniawan.foundation", name = "core-analytics", version.ref = "wisnuFoundation" } +wisnu-foundation-core-datetime = { group = "com.github.wisnukurniawan.foundation", name = "core-date-time", version.ref = "wisnuFoundation" } +wisnu-foundation-core-json = { group = "com.github.wisnukurniawan.foundation", name = "core-json", version.ref = "wisnuFoundation" } +wisnu-foundation-core-loggr = { group = "com.github.wisnukurniawan.foundation", name = "core-loggr", version.ref = "wisnuFoundation" } +wisnu-foundation-core-ui = { group = "com.github.wisnukurniawan.foundation", name = "core-ui", version.ref = "wisnuFoundation" } +wisnu-foundation-core-viewmodel = { group = "com.github.wisnukurniawan.foundation", name = "core-viewmodel", version.ref = "wisnuFoundation" } +wisnu-foundation-lib-analyticsmanager = { group = "com.github.wisnukurniawan.foundation", name = "lib-analytics-manager", version.ref = "wisnuFoundation" } +wisnu-foundation-lib-lifecycleloggr = { group = "com.github.wisnukurniawan.foundation", name = "lib-lifecycle-loggr", version.ref = "wisnuFoundation" } +wisnu-foundation-test-debugnoop = { group = "com.github.wisnukurniawan.foundation", name = "test-debug-no-op", version.ref = "wisnuFoundation" } +wisnu-foundation-test-debug = { group = "com.github.wisnukurniawan.foundation", name = "test-debug", version.ref = "wisnuFoundation" } + +debugging-chucker = { group = "com.github.chuckerteam.chucker", name = "library", version = "4.0.0" } +debugging-compose-uiTooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidxCompose" } +debugging-okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } + +test-androidx-core = { group = "androidx.test", name = "core", version = "1.6.1" } +test-androidx-espressoCore = { group = "androidx.test.espresso", name = "espresso-core", version = "3.6.1" } +test-androidx-ext = { group = "androidx.test.ext", name = "junit-ktx", version = "1.2.1" } +test-androidx-junit = { group = "androidx.test.ext", name = "junit", version = "1.2.1" } +test-androidx-runner = { group = "androidx.test", name = "runner", version = "1.6.1" } +test-androidx-rules = { group = "androidx.test", name = "rules", version = "1.6.1" } +test-androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version = "2.3.0" } +test-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } +test-junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "jUnit" } +test-junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "jUnit" } +test-junit-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "jUnit" } +test-junit-old = { group = "junit", name = "junit", version = "4.13.2" } +test-junit-oldParams = { group = "pl.pragmatists", name = "JUnitParams", version = "1.1.1" } +test-robolectric = { group = "org.robolectric", name = "robolectric", version = "4.12.1" } +test-turbine = { group = "app.cash.turbine", name = "turbine", version = "1.1.0" } + + +[bundles] +networking = ["squareup-retrofit-core", "squareup-retrofit-converter", "squareup-okhttp"]